OLD | NEW |
| (Empty) |
1 /* | |
2 * Copyright (C) 2012, Google Inc. All rights reserved. | |
3 * | |
4 * Redistribution and use in source and binary forms, with or without | |
5 * modification, are permitted provided that the following conditions | |
6 * are met: | |
7 * | |
8 * 1. Redistributions of source code must retain the above copyright | |
9 * notice, this list of conditions and the following disclaimer. | |
10 * 2. Redistributions in binary form must reproduce the above copyright | |
11 * notice, this list of conditions and the following disclaimer in the | |
12 * documentation and/or other materials provided with the distribution. | |
13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of | |
14 * its contributors may be used to endorse or promote products derived | |
15 * from this software without specific prior written permission. | |
16 * | |
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY | |
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY | |
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | |
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
27 */ | |
28 | |
29 #include "config.h" | |
30 #include "core/accessibility/AXNodeObject.h" | |
31 | |
32 #include "core/InputTypeNames.h" | |
33 #include "core/accessibility/AXObjectCacheImpl.h" | |
34 #include "core/dom/NodeTraversal.h" | |
35 #include "core/dom/Text.h" | |
36 #include "core/html/HTMLDListElement.h" | |
37 #include "core/html/HTMLFieldSetElement.h" | |
38 #include "core/html/HTMLFrameElementBase.h" | |
39 #include "core/html/HTMLInputElement.h" | |
40 #include "core/html/HTMLLabelElement.h" | |
41 #include "core/html/HTMLLegendElement.h" | |
42 #include "core/html/HTMLPlugInElement.h" | |
43 #include "core/html/HTMLSelectElement.h" | |
44 #include "core/html/HTMLTextAreaElement.h" | |
45 #include "core/rendering/RenderObject.h" | |
46 #include "platform/UserGestureIndicator.h" | |
47 #include "wtf/text/StringBuilder.h" | |
48 | |
49 | |
50 namespace blink { | |
51 | |
52 using namespace HTMLNames; | |
53 | |
54 AXNodeObject::AXNodeObject(Node* node) | |
55 : AXObject() | |
56 , m_ariaRole(UnknownRole) | |
57 , m_childrenDirty(false) | |
58 #if ENABLE(ASSERT) | |
59 , m_initialized(false) | |
60 #endif | |
61 , m_node(node) | |
62 { | |
63 } | |
64 | |
65 PassRefPtr<AXNodeObject> AXNodeObject::create(Node* node) | |
66 { | |
67 return adoptRef(new AXNodeObject(node)); | |
68 } | |
69 | |
70 AXNodeObject::~AXNodeObject() | |
71 { | |
72 ASSERT(isDetached()); | |
73 } | |
74 | |
75 // This function implements the ARIA accessible name as described by the Mozilla | |
76 // ARIA Implementer's Guide. | |
77 static String accessibleNameForNode(Node* node) | |
78 { | |
79 if (!node) | |
80 return String(); | |
81 | |
82 if (node->isTextNode()) | |
83 return toText(node)->data(); | |
84 | |
85 if (isHTMLInputElement(*node)) | |
86 return toHTMLInputElement(*node).value(); | |
87 | |
88 if (node->isHTMLElement()) { | |
89 const AtomicString& alt = toHTMLElement(node)->getAttribute(altAttr); | |
90 if (!alt.isEmpty()) | |
91 return alt; | |
92 } | |
93 | |
94 return String(); | |
95 } | |
96 | |
97 String AXNodeObject::accessibilityDescriptionForElements(WillBeHeapVector<RawPtr
WillBeMember<Element> > &elements) const | |
98 { | |
99 StringBuilder builder; | |
100 unsigned size = elements.size(); | |
101 for (unsigned i = 0; i < size; ++i) { | |
102 Element* idElement = elements[i]; | |
103 | |
104 builder.append(accessibleNameForNode(idElement)); | |
105 for (Node& n : NodeTraversal::descendantsOf(*idElement)) | |
106 builder.append(accessibleNameForNode(&n)); | |
107 | |
108 if (i != size - 1) | |
109 builder.append(' '); | |
110 } | |
111 return builder.toString(); | |
112 } | |
113 | |
114 void AXNodeObject::alterSliderValue(bool increase) | |
115 { | |
116 if (roleValue() != SliderRole) | |
117 return; | |
118 | |
119 if (!getAttribute(stepAttr).isEmpty()) | |
120 changeValueByStep(increase); | |
121 else | |
122 changeValueByPercent(increase ? 5 : -5); | |
123 } | |
124 | |
125 String AXNodeObject::ariaAccessibilityDescription() const | |
126 { | |
127 String ariaLabeledBy = ariaLabeledByAttribute(); | |
128 if (!ariaLabeledBy.isEmpty()) | |
129 return ariaLabeledBy; | |
130 | |
131 const AtomicString& ariaLabel = getAttribute(aria_labelAttr); | |
132 if (!ariaLabel.isEmpty()) | |
133 return ariaLabel; | |
134 | |
135 return String(); | |
136 } | |
137 | |
138 | |
139 void AXNodeObject::ariaLabeledByElements(WillBeHeapVector<RawPtrWillBeMember<Ele
ment> >& elements) const | |
140 { | |
141 elementsFromAttribute(elements, aria_labeledbyAttr); | |
142 if (!elements.size()) | |
143 elementsFromAttribute(elements, aria_labelledbyAttr); | |
144 } | |
145 | |
146 void AXNodeObject::changeValueByStep(bool increase) | |
147 { | |
148 float step = stepValueForRange(); | |
149 float value = valueForRange(); | |
150 | |
151 value += increase ? step : -step; | |
152 | |
153 setValue(String::number(value)); | |
154 | |
155 axObjectCache()->postNotification(node(), AXObjectCacheImpl::AXValueChanged,
true); | |
156 } | |
157 | |
158 bool AXNodeObject::computeAccessibilityIsIgnored() const | |
159 { | |
160 #if ENABLE(ASSERT) | |
161 // Double-check that an AXObject is never accessed before | |
162 // it's been initialized. | |
163 ASSERT(m_initialized); | |
164 #endif | |
165 | |
166 // If this element is within a parent that cannot have children, it should n
ot be exposed. | |
167 if (isDescendantOfBarrenParent()) | |
168 return true; | |
169 | |
170 // Ignore labels that are already referenced by a control's title UI element
. | |
171 AXObject* controlObject = correspondingControlForLabelElement(); | |
172 if (controlObject && !controlObject->exposesTitleUIElement() && controlObjec
t->isCheckboxOrRadio()) | |
173 return true; | |
174 | |
175 return m_role == UnknownRole; | |
176 } | |
177 | |
178 AccessibilityRole AXNodeObject::determineAccessibilityRole() | |
179 { | |
180 if (!node()) | |
181 return UnknownRole; | |
182 | |
183 m_ariaRole = determineAriaRoleAttribute(); | |
184 | |
185 AccessibilityRole ariaRole = ariaRoleAttribute(); | |
186 if (ariaRole != UnknownRole) | |
187 return ariaRole; | |
188 | |
189 if (node()->isLink()) | |
190 return LinkRole; | |
191 if (node()->isTextNode()) | |
192 return StaticTextRole; | |
193 if (isHTMLButtonElement(*node())) | |
194 return buttonRoleType(); | |
195 if (isHTMLDetailsElement(*node())) | |
196 return DetailsRole; | |
197 if (isHTMLSummaryElement(*node())) { | |
198 if (node()->parentNode() && isHTMLDetailsElement(node()->parentNode())) | |
199 return DisclosureTriangleRole; | |
200 return UnknownRole; | |
201 } | |
202 | |
203 if (isHTMLInputElement(*node())) { | |
204 HTMLInputElement& input = toHTMLInputElement(*node()); | |
205 const AtomicString& type = input.type(); | |
206 if (type == InputTypeNames::button) { | |
207 if ((node()->parentNode() && isHTMLMenuElement(node()->parentNode())
) || (parentObject() && parentObject()->roleValue() == MenuRole)) | |
208 return MenuItemRole; | |
209 return buttonRoleType(); | |
210 } | |
211 if (type == InputTypeNames::checkbox) { | |
212 if ((node()->parentNode() && isHTMLMenuElement(node()->parentNode())
) || (parentObject() && parentObject()->roleValue() == MenuRole)) | |
213 return MenuItemCheckBoxRole; | |
214 return CheckBoxRole; | |
215 } | |
216 if (type == InputTypeNames::date) | |
217 return DateRole; | |
218 if (type == InputTypeNames::datetime | |
219 || type == InputTypeNames::datetime_local | |
220 || type == InputTypeNames::month | |
221 || type == InputTypeNames::week) | |
222 return DateTimeRole; | |
223 if (type == InputTypeNames::radio) { | |
224 if ((node()->parentNode() && isHTMLMenuElement(node()->parentNode())
) || (parentObject() && parentObject()->roleValue() == MenuRole)) | |
225 return MenuItemRadioRole; | |
226 return RadioButtonRole; | |
227 } | |
228 if (input.isTextButton()) | |
229 return buttonRoleType(); | |
230 if (type == InputTypeNames::range) | |
231 return SliderRole; | |
232 if (type == InputTypeNames::color) | |
233 return ColorWellRole; | |
234 if (type == InputTypeNames::time) | |
235 return TimeRole; | |
236 return TextFieldRole; | |
237 } | |
238 if (isHTMLSelectElement(*node())) { | |
239 HTMLSelectElement& selectElement = toHTMLSelectElement(*node()); | |
240 return selectElement.multiple() ? ListBoxRole : PopUpButtonRole; | |
241 } | |
242 if (isHTMLTextAreaElement(*node())) | |
243 return TextAreaRole; | |
244 if (headingLevel()) | |
245 return HeadingRole; | |
246 if (isHTMLDivElement(*node())) | |
247 return DivRole; | |
248 if (isHTMLMeterElement(*node())) | |
249 return MeterRole; | |
250 if (isHTMLOutputElement(*node())) | |
251 return StatusRole; | |
252 if (isHTMLParagraphElement(*node())) | |
253 return ParagraphRole; | |
254 if (isHTMLLabelElement(*node())) | |
255 return LabelRole; | |
256 if (isHTMLRubyElement(*node())) | |
257 return RubyRole; | |
258 if (isHTMLDListElement(*node())) | |
259 return DescriptionListRole; | |
260 if (node()->isElementNode() && node()->hasTagName(blockquoteTag)) | |
261 return BlockquoteRole; | |
262 if (node()->isElementNode() && node()->hasTagName(figcaptionTag)) | |
263 return FigcaptionRole; | |
264 if (node()->isElementNode() && node()->hasTagName(figureTag)) | |
265 return FigureRole; | |
266 if (node()->isElementNode() && toElement(node())->isFocusable()) | |
267 return GroupRole; | |
268 if (isHTMLAnchorElement(*node()) && isClickable()) | |
269 return LinkRole; | |
270 if (isHTMLIFrameElement(*node())) | |
271 return IframeRole; | |
272 if (isEmbeddedObject()) | |
273 return EmbeddedObjectRole; | |
274 | |
275 return UnknownRole; | |
276 } | |
277 | |
278 AccessibilityRole AXNodeObject::determineAriaRoleAttribute() const | |
279 { | |
280 const AtomicString& ariaRole = getAttribute(roleAttr); | |
281 if (ariaRole.isNull() || ariaRole.isEmpty()) | |
282 return UnknownRole; | |
283 | |
284 AccessibilityRole role = ariaRoleToWebCoreRole(ariaRole); | |
285 | |
286 // ARIA states if an item can get focus, it should not be presentational. | |
287 if ((role == NoneRole || role == PresentationalRole) && canSetFocusAttribute
()) | |
288 return UnknownRole; | |
289 | |
290 if (role == ButtonRole) | |
291 role = buttonRoleType(); | |
292 | |
293 if (role == TextAreaRole && !ariaIsMultiline()) | |
294 role = TextFieldRole; | |
295 | |
296 role = remapAriaRoleDueToParent(role); | |
297 | |
298 if (role) | |
299 return role; | |
300 | |
301 return UnknownRole; | |
302 } | |
303 | |
304 void AXNodeObject::elementsFromAttribute(WillBeHeapVector<RawPtrWillBeMember<Ele
ment> >& elements, const QualifiedName& attribute) const | |
305 { | |
306 Node* node = this->node(); | |
307 if (!node || !node->isElementNode()) | |
308 return; | |
309 | |
310 TreeScope& scope = node->treeScope(); | |
311 | |
312 String idList = getAttribute(attribute).string(); | |
313 if (idList.isEmpty()) | |
314 return; | |
315 | |
316 idList.replace('\n', ' '); | |
317 Vector<String> idVector; | |
318 idList.split(' ', idVector); | |
319 | |
320 unsigned size = idVector.size(); | |
321 for (unsigned i = 0; i < size; ++i) { | |
322 AtomicString idName(idVector[i]); | |
323 Element* idElement = scope.getElementById(idName); | |
324 if (idElement) | |
325 elements.append(idElement); | |
326 } | |
327 } | |
328 | |
329 // If you call node->hasEditableStyle() since that will return true if an ancest
or is editable. | |
330 // This only returns true if this is the element that actually has the contentEd
itable attribute set. | |
331 bool AXNodeObject::hasContentEditableAttributeSet() const | |
332 { | |
333 if (!hasAttribute(contenteditableAttr)) | |
334 return false; | |
335 const AtomicString& contentEditableValue = getAttribute(contenteditableAttr)
; | |
336 // Both "true" (case-insensitive) and the empty string count as true. | |
337 return contentEditableValue.isEmpty() || equalIgnoringCase(contentEditableVa
lue, "true"); | |
338 } | |
339 | |
340 bool AXNodeObject::isDescendantOfBarrenParent() const | |
341 { | |
342 for (AXObject* object = parentObject(); object; object = object->parentObjec
t()) { | |
343 if (!object->canHaveChildren()) | |
344 return true; | |
345 } | |
346 | |
347 return false; | |
348 } | |
349 | |
350 bool AXNodeObject::isGenericFocusableElement() const | |
351 { | |
352 if (!canSetFocusAttribute()) | |
353 return false; | |
354 | |
355 // If it's a control, it's not generic. | |
356 if (isControl()) | |
357 return false; | |
358 | |
359 // If it has an aria role, it's not generic. | |
360 if (m_ariaRole != UnknownRole) | |
361 return false; | |
362 | |
363 // If the content editable attribute is set on this element, that's the reas
on | |
364 // it's focusable, and existing logic should handle this case already - so i
t's not a | |
365 // generic focusable element. | |
366 | |
367 if (hasContentEditableAttributeSet()) | |
368 return false; | |
369 | |
370 // The web area and body element are both focusable, but existing logic hand
les these | |
371 // cases already, so we don't need to include them here. | |
372 if (roleValue() == WebAreaRole) | |
373 return false; | |
374 if (isHTMLBodyElement(node())) | |
375 return false; | |
376 | |
377 // An SVG root is focusable by default, but it's probably not interactive, s
o don't | |
378 // include it. It can still be made accessible by giving it an ARIA role. | |
379 if (roleValue() == SVGRootRole) | |
380 return false; | |
381 | |
382 return true; | |
383 } | |
384 | |
385 HTMLLabelElement* AXNodeObject::labelForElement(Element* element) const | |
386 { | |
387 if (!element->isHTMLElement() || !toHTMLElement(element)->isLabelable()) | |
388 return 0; | |
389 | |
390 const AtomicString& id = element->getIdAttribute(); | |
391 if (!id.isEmpty()) { | |
392 if (HTMLLabelElement* label = element->treeScope().labelElementForId(id)
) | |
393 return label; | |
394 } | |
395 | |
396 return Traversal<HTMLLabelElement>::firstAncestor(*element); | |
397 } | |
398 | |
399 AXObject* AXNodeObject::menuButtonForMenu() const | |
400 { | |
401 Element* menuItem = menuItemElementForMenu(); | |
402 | |
403 if (menuItem) { | |
404 // ARIA just has generic menu items. AppKit needs to know if this is a t
op level items like MenuBarButton or MenuBarItem | |
405 AXObject* menuItemAX = axObjectCache()->getOrCreate(menuItem); | |
406 if (menuItemAX && menuItemAX->isMenuButton()) | |
407 return menuItemAX; | |
408 } | |
409 return 0; | |
410 } | |
411 | |
412 static Element* siblingWithAriaRole(String role, Node* node) | |
413 { | |
414 Node* parent = node->parentNode(); | |
415 if (!parent) | |
416 return 0; | |
417 | |
418 for (Element* sibling = ElementTraversal::firstChild(*parent); sibling; sibl
ing = ElementTraversal::nextSibling(*sibling)) { | |
419 const AtomicString& siblingAriaRole = sibling->getAttribute(roleAttr); | |
420 if (equalIgnoringCase(siblingAriaRole, role)) | |
421 return sibling; | |
422 } | |
423 | |
424 return 0; | |
425 } | |
426 | |
427 Element* AXNodeObject::menuItemElementForMenu() const | |
428 { | |
429 if (ariaRoleAttribute() != MenuRole) | |
430 return 0; | |
431 | |
432 return siblingWithAriaRole("menuitem", node()); | |
433 } | |
434 | |
435 Element* AXNodeObject::mouseButtonListener() const | |
436 { | |
437 Node* node = this->node(); | |
438 if (!node) | |
439 return 0; | |
440 | |
441 // check if our parent is a mouse button listener | |
442 if (!node->isElementNode()) | |
443 node = node->parentElement(); | |
444 | |
445 if (!node) | |
446 return 0; | |
447 | |
448 // FIXME: Do the continuation search like anchorElement does | |
449 for (Element* element = toElement(node); element; element = element->parentE
lement()) { | |
450 if (element->getAttributeEventListener(EventTypeNames::click) || element
->getAttributeEventListener(EventTypeNames::mousedown) || element->getAttributeE
ventListener(EventTypeNames::mouseup)) | |
451 return element; | |
452 } | |
453 | |
454 return 0; | |
455 } | |
456 | |
457 AccessibilityRole AXNodeObject::remapAriaRoleDueToParent(AccessibilityRole role)
const | |
458 { | |
459 // Some objects change their role based on their parent. | |
460 // However, asking for the unignoredParent calls accessibilityIsIgnored(), w
hich can trigger a loop. | |
461 // While inside the call stack of creating an element, we need to avoid acce
ssibilityIsIgnored(). | |
462 // https://bugs.webkit.org/show_bug.cgi?id=65174 | |
463 | |
464 if (role != ListBoxOptionRole && role != MenuItemRole) | |
465 return role; | |
466 | |
467 for (AXObject* parent = parentObject(); parent && !parent->accessibilityIsIg
nored(); parent = parent->parentObject()) { | |
468 AccessibilityRole parentAriaRole = parent->ariaRoleAttribute(); | |
469 | |
470 // Selects and listboxes both have options as child roles, but they map
to different roles within WebCore. | |
471 if (role == ListBoxOptionRole && parentAriaRole == MenuRole) | |
472 return MenuItemRole; | |
473 // An aria "menuitem" may map to MenuButton or MenuItem depending on its
parent. | |
474 if (role == MenuItemRole && parentAriaRole == GroupRole) | |
475 return MenuButtonRole; | |
476 | |
477 // If the parent had a different role, then we don't need to continue se
arching up the chain. | |
478 if (parentAriaRole) | |
479 break; | |
480 } | |
481 | |
482 return role; | |
483 } | |
484 | |
485 void AXNodeObject::init() | |
486 { | |
487 #if ENABLE(ASSERT) | |
488 ASSERT(!m_initialized); | |
489 m_initialized = true; | |
490 #endif | |
491 m_role = determineAccessibilityRole(); | |
492 } | |
493 | |
494 void AXNodeObject::detach() | |
495 { | |
496 clearChildren(); | |
497 AXObject::detach(); | |
498 m_node = 0; | |
499 } | |
500 | |
501 bool AXNodeObject::isAnchor() const | |
502 { | |
503 return !isNativeImage() && isLink(); | |
504 } | |
505 | |
506 bool AXNodeObject::isControl() const | |
507 { | |
508 Node* node = this->node(); | |
509 if (!node) | |
510 return false; | |
511 | |
512 return ((node->isElementNode() && toElement(node)->isFormControlElement()) | |
513 || AXObject::isARIAControl(ariaRoleAttribute())); | |
514 } | |
515 | |
516 bool AXNodeObject::isEmbeddedObject() const | |
517 { | |
518 return isHTMLPlugInElement(node()); | |
519 } | |
520 | |
521 bool AXNodeObject::isFieldset() const | |
522 { | |
523 return isHTMLFieldSetElement(node()); | |
524 } | |
525 | |
526 bool AXNodeObject::isHeading() const | |
527 { | |
528 return roleValue() == HeadingRole; | |
529 } | |
530 | |
531 bool AXNodeObject::isHovered() const | |
532 { | |
533 Node* node = this->node(); | |
534 if (!node) | |
535 return false; | |
536 | |
537 return node->hovered(); | |
538 } | |
539 | |
540 bool AXNodeObject::isImage() const | |
541 { | |
542 return roleValue() == ImageRole; | |
543 } | |
544 | |
545 bool AXNodeObject::isImageButton() const | |
546 { | |
547 return isNativeImage() && isButton(); | |
548 } | |
549 | |
550 bool AXNodeObject::isInputImage() const | |
551 { | |
552 Node* node = this->node(); | |
553 if (roleValue() == ButtonRole && isHTMLInputElement(node)) | |
554 return toHTMLInputElement(*node).type() == InputTypeNames::image; | |
555 | |
556 return false; | |
557 } | |
558 | |
559 bool AXNodeObject::isLink() const | |
560 { | |
561 return roleValue() == LinkRole; | |
562 } | |
563 | |
564 bool AXNodeObject::isMenu() const | |
565 { | |
566 return roleValue() == MenuRole; | |
567 } | |
568 | |
569 bool AXNodeObject::isMenuButton() const | |
570 { | |
571 return roleValue() == MenuButtonRole; | |
572 } | |
573 | |
574 bool AXNodeObject::isMultiSelectable() const | |
575 { | |
576 const AtomicString& ariaMultiSelectable = getAttribute(aria_multiselectableA
ttr); | |
577 if (equalIgnoringCase(ariaMultiSelectable, "true")) | |
578 return true; | |
579 if (equalIgnoringCase(ariaMultiSelectable, "false")) | |
580 return false; | |
581 | |
582 return isHTMLSelectElement(node()) && toHTMLSelectElement(*node()).multiple(
); | |
583 } | |
584 | |
585 bool AXNodeObject::isNativeCheckboxOrRadio() const | |
586 { | |
587 Node* node = this->node(); | |
588 if (!isHTMLInputElement(node)) | |
589 return false; | |
590 | |
591 HTMLInputElement* input = toHTMLInputElement(node); | |
592 return input->type() == InputTypeNames::checkbox || input->type() == InputTy
peNames::radio; | |
593 } | |
594 | |
595 bool AXNodeObject::isNativeImage() const | |
596 { | |
597 Node* node = this->node(); | |
598 if (!node) | |
599 return false; | |
600 | |
601 if (isHTMLImageElement(*node)) | |
602 return true; | |
603 | |
604 if (isHTMLPlugInElement(*node)) | |
605 return true; | |
606 | |
607 if (isHTMLInputElement(*node)) | |
608 return toHTMLInputElement(*node).type() == InputTypeNames::image; | |
609 | |
610 return false; | |
611 } | |
612 | |
613 bool AXNodeObject::isNativeTextControl() const | |
614 { | |
615 Node* node = this->node(); | |
616 if (!node) | |
617 return false; | |
618 | |
619 if (isHTMLTextAreaElement(*node)) | |
620 return true; | |
621 | |
622 if (isHTMLInputElement(*node)) | |
623 return toHTMLInputElement(node)->isTextField(); | |
624 | |
625 return false; | |
626 } | |
627 | |
628 bool AXNodeObject::isNonNativeTextControl() const | |
629 { | |
630 if (isNativeTextControl()) | |
631 return false; | |
632 | |
633 if (hasContentEditableAttributeSet()) | |
634 return true; | |
635 | |
636 if (isARIATextControl()) | |
637 return true; | |
638 | |
639 return false; | |
640 } | |
641 | |
642 bool AXNodeObject::isPasswordField() const | |
643 { | |
644 Node* node = this->node(); | |
645 if (!isHTMLInputElement(node)) | |
646 return false; | |
647 | |
648 if (ariaRoleAttribute() != UnknownRole) | |
649 return false; | |
650 | |
651 return toHTMLInputElement(node)->type() == InputTypeNames::password; | |
652 } | |
653 | |
654 bool AXNodeObject::isProgressIndicator() const | |
655 { | |
656 return roleValue() == ProgressIndicatorRole; | |
657 } | |
658 | |
659 bool AXNodeObject::isSlider() const | |
660 { | |
661 return roleValue() == SliderRole; | |
662 } | |
663 | |
664 bool AXNodeObject::isChecked() const | |
665 { | |
666 Node* node = this->node(); | |
667 if (!node) | |
668 return false; | |
669 | |
670 // First test for native checkedness semantics | |
671 if (isHTMLInputElement(*node)) | |
672 return toHTMLInputElement(*node).shouldAppearChecked(); | |
673 | |
674 // Else, if this is an ARIA checkbox or radio OR ARIA role menuitemcheckbox | |
675 // or menuitemradio, respect the aria-checked attribute | |
676 AccessibilityRole ariaRole = ariaRoleAttribute(); | |
677 if (ariaRole == RadioButtonRole || ariaRole == CheckBoxRole | |
678 || ariaRole == MenuItemCheckBoxRole || ariaRole == MenuItemRadioRole) { | |
679 if (equalIgnoringCase(getAttribute(aria_checkedAttr), "true")) | |
680 return true; | |
681 return false; | |
682 } | |
683 | |
684 // Otherwise it's not checked | |
685 return false; | |
686 } | |
687 | |
688 bool AXNodeObject::isClickable() const | |
689 { | |
690 if (node()) { | |
691 if (node()->isElementNode() && toElement(node())->isDisabledFormControl(
)) | |
692 return false; | |
693 | |
694 // Note: we can't call node()->willRespondToMouseClickEvents() because t
hat triggers a style recalc and can delete this. | |
695 if (node()->hasEventListeners(EventTypeNames::mouseup) || node()->hasEve
ntListeners(EventTypeNames::mousedown) || node()->hasEventListeners(EventTypeNam
es::click) || node()->hasEventListeners(EventTypeNames::DOMActivate)) | |
696 return true; | |
697 } | |
698 | |
699 return AXObject::isClickable(); | |
700 } | |
701 | |
702 bool AXNodeObject::isEnabled() const | |
703 { | |
704 if (equalIgnoringCase(getAttribute(aria_disabledAttr), "true")) | |
705 return false; | |
706 | |
707 Node* node = this->node(); | |
708 if (!node || !node->isElementNode()) | |
709 return true; | |
710 | |
711 return !toElement(node)->isDisabledFormControl(); | |
712 } | |
713 | |
714 AccessibilityExpanded AXNodeObject::isExpanded() const | |
715 { | |
716 const AtomicString& expanded = getAttribute(aria_expandedAttr); | |
717 if (equalIgnoringCase(expanded, "true")) | |
718 return ExpandedExpanded; | |
719 if (equalIgnoringCase(expanded, "false")) | |
720 return ExpandedCollapsed; | |
721 | |
722 return ExpandedUndefined; | |
723 } | |
724 | |
725 bool AXNodeObject::isIndeterminate() const | |
726 { | |
727 Node* node = this->node(); | |
728 if (!isHTMLInputElement(node)) | |
729 return false; | |
730 | |
731 return toHTMLInputElement(node)->shouldAppearIndeterminate(); | |
732 } | |
733 | |
734 bool AXNodeObject::isPressed() const | |
735 { | |
736 if (!isButton()) | |
737 return false; | |
738 | |
739 Node* node = this->node(); | |
740 if (!node) | |
741 return false; | |
742 | |
743 // If this is an ARIA button, check the aria-pressed attribute rather than n
ode()->active() | |
744 if (ariaRoleAttribute() == ButtonRole) { | |
745 if (equalIgnoringCase(getAttribute(aria_pressedAttr), "true")) | |
746 return true; | |
747 return false; | |
748 } | |
749 | |
750 return node->active(); | |
751 } | |
752 | |
753 bool AXNodeObject::isReadOnly() const | |
754 { | |
755 Node* node = this->node(); | |
756 if (!node) | |
757 return true; | |
758 | |
759 if (isHTMLTextAreaElement(*node)) | |
760 return toHTMLTextAreaElement(*node).isReadOnly(); | |
761 | |
762 if (isHTMLInputElement(*node)) { | |
763 HTMLInputElement& input = toHTMLInputElement(*node); | |
764 if (input.isTextField()) | |
765 return input.isReadOnly(); | |
766 } | |
767 | |
768 return !node->hasEditableStyle(); | |
769 } | |
770 | |
771 bool AXNodeObject::isRequired() const | |
772 { | |
773 if (equalIgnoringCase(getAttribute(aria_requiredAttr), "true")) | |
774 return true; | |
775 | |
776 Node* n = this->node(); | |
777 if (n && (n->isElementNode() && toElement(n)->isFormControlElement())) | |
778 return toHTMLFormControlElement(n)->isRequired(); | |
779 | |
780 return false; | |
781 } | |
782 | |
783 bool AXNodeObject::canSetFocusAttribute() const | |
784 { | |
785 Node* node = this->node(); | |
786 if (!node) | |
787 return false; | |
788 | |
789 if (isWebArea()) | |
790 return true; | |
791 | |
792 // NOTE: It would be more accurate to ask the document whether setFocusedNod
e() would | |
793 // do anything. For example, setFocusedNode() will do nothing if the current
focused | |
794 // node will not relinquish the focus. | |
795 if (!node) | |
796 return false; | |
797 | |
798 if (isDisabledFormControl(node)) | |
799 return false; | |
800 | |
801 return node->isElementNode() && toElement(node)->supportsFocus(); | |
802 } | |
803 | |
804 bool AXNodeObject::canSetValueAttribute() const | |
805 { | |
806 if (equalIgnoringCase(getAttribute(aria_readonlyAttr), "true")) | |
807 return false; | |
808 | |
809 if (isProgressIndicator() || isSlider()) | |
810 return true; | |
811 | |
812 if (isTextControl() && !isNativeTextControl()) | |
813 return true; | |
814 | |
815 // Any node could be contenteditable, so isReadOnly should be relied upon | |
816 // for this information for all elements. | |
817 return !isReadOnly(); | |
818 } | |
819 | |
820 bool AXNodeObject::canvasHasFallbackContent() const | |
821 { | |
822 Node* node = this->node(); | |
823 if (!isHTMLCanvasElement(node)) | |
824 return false; | |
825 | |
826 // If it has any children that are elements, we'll assume it might be fallba
ck | |
827 // content. If it has no children or its only children are not elements | |
828 // (e.g. just text nodes), it doesn't have fallback content. | |
829 return ElementTraversal::firstChild(*node); | |
830 } | |
831 | |
832 bool AXNodeObject::exposesTitleUIElement() const | |
833 { | |
834 if (!isControl()) | |
835 return false; | |
836 | |
837 // If this control is ignored (because it's invisible), | |
838 // then the label needs to be exposed so it can be visible to accessibility. | |
839 if (accessibilityIsIgnored()) | |
840 return true; | |
841 | |
842 // ARIA: section 2A, bullet #3 says if aria-labeledby or aria-label appears,
it should | |
843 // override the "label" element association. | |
844 bool hasTextAlternative = (!ariaLabeledByAttribute().isEmpty() || !getAttrib
ute(aria_labelAttr).isEmpty()); | |
845 | |
846 // Checkboxes and radio buttons use the text of their title ui element as th
eir own AXTitle. | |
847 // This code controls whether the title ui element should appear in the AX t
ree (usually, no). | |
848 // It should appear if the control already has a label (which will be used a
s the AXTitle instead). | |
849 if (isCheckboxOrRadio()) | |
850 return hasTextAlternative; | |
851 | |
852 // When controls have their own descriptions, the title element should be ig
nored. | |
853 if (hasTextAlternative) | |
854 return false; | |
855 | |
856 return true; | |
857 } | |
858 | |
859 int AXNodeObject::headingLevel() const | |
860 { | |
861 // headings can be in block flow and non-block flow | |
862 Node* node = this->node(); | |
863 if (!node) | |
864 return 0; | |
865 | |
866 if (ariaRoleAttribute() == HeadingRole) | |
867 return getAttribute(aria_levelAttr).toInt(); | |
868 | |
869 if (!node->isHTMLElement()) | |
870 return 0; | |
871 | |
872 HTMLElement& element = toHTMLElement(*node); | |
873 if (element.hasTagName(h1Tag)) | |
874 return 1; | |
875 | |
876 if (element.hasTagName(h2Tag)) | |
877 return 2; | |
878 | |
879 if (element.hasTagName(h3Tag)) | |
880 return 3; | |
881 | |
882 if (element.hasTagName(h4Tag)) | |
883 return 4; | |
884 | |
885 if (element.hasTagName(h5Tag)) | |
886 return 5; | |
887 | |
888 if (element.hasTagName(h6Tag)) | |
889 return 6; | |
890 | |
891 return 0; | |
892 } | |
893 | |
894 unsigned AXNodeObject::hierarchicalLevel() const | |
895 { | |
896 Node* node = this->node(); | |
897 if (!node || !node->isElementNode()) | |
898 return 0; | |
899 Element* element = toElement(node); | |
900 String ariaLevel = element->getAttribute(aria_levelAttr); | |
901 if (!ariaLevel.isEmpty()) | |
902 return ariaLevel.toInt(); | |
903 | |
904 // Only tree item will calculate its level through the DOM currently. | |
905 if (roleValue() != TreeItemRole) | |
906 return 0; | |
907 | |
908 // Hierarchy leveling starts at 1, to match the aria-level spec. | |
909 // We measure tree hierarchy by the number of groups that the item is within
. | |
910 unsigned level = 1; | |
911 for (AXObject* parent = parentObject(); parent; parent = parent->parentObjec
t()) { | |
912 AccessibilityRole parentRole = parent->roleValue(); | |
913 if (parentRole == GroupRole) | |
914 level++; | |
915 else if (parentRole == TreeRole) | |
916 break; | |
917 } | |
918 | |
919 return level; | |
920 } | |
921 | |
922 String AXNodeObject::text() const | |
923 { | |
924 // If this is a user defined static text, use the accessible name computatio
n. | |
925 if (ariaRoleAttribute() == StaticTextRole) | |
926 return ariaAccessibilityDescription(); | |
927 | |
928 if (!isTextControl()) | |
929 return String(); | |
930 | |
931 Node* node = this->node(); | |
932 if (!node) | |
933 return String(); | |
934 | |
935 if (isNativeTextControl() && (isHTMLTextAreaElement(*node) || isHTMLInputEle
ment(*node))) | |
936 return toHTMLTextFormControlElement(*node).value(); | |
937 | |
938 if (!node->isElementNode()) | |
939 return String(); | |
940 | |
941 return toElement(node)->innerText(); | |
942 } | |
943 | |
944 AXObject* AXNodeObject::titleUIElement() const | |
945 { | |
946 if (!node() || !node()->isElementNode()) | |
947 return 0; | |
948 | |
949 if (isFieldset()) | |
950 return axObjectCache()->getOrCreate(toHTMLFieldSetElement(node())->legen
d()); | |
951 | |
952 HTMLLabelElement* label = labelForElement(toElement(node())); | |
953 if (label) | |
954 return axObjectCache()->getOrCreate(label); | |
955 | |
956 return 0; | |
957 } | |
958 | |
959 AccessibilityButtonState AXNodeObject::checkboxOrRadioValue() const | |
960 { | |
961 if (isNativeCheckboxOrRadio()) | |
962 return isChecked() ? ButtonStateOn : ButtonStateOff; | |
963 | |
964 return AXObject::checkboxOrRadioValue(); | |
965 } | |
966 | |
967 void AXNodeObject::colorValue(int& r, int& g, int& b) const | |
968 { | |
969 r = 0; | |
970 g = 0; | |
971 b = 0; | |
972 | |
973 if (!isColorWell()) | |
974 return; | |
975 | |
976 if (!isHTMLInputElement(node())) | |
977 return; | |
978 | |
979 HTMLInputElement* input = toHTMLInputElement(node()); | |
980 const AtomicString& type = input->getAttribute(typeAttr); | |
981 if (!equalIgnoringCase(type, "color")) | |
982 return; | |
983 | |
984 // HTMLInputElement::value always returns a string parseable by Color. | |
985 Color color; | |
986 bool success = color.setFromString(input->value()); | |
987 ASSERT_UNUSED(success, success); | |
988 r = color.red(); | |
989 g = color.green(); | |
990 b = color.blue(); | |
991 } | |
992 | |
993 String AXNodeObject::valueDescription() const | |
994 { | |
995 if (!supportsRangeValue()) | |
996 return String(); | |
997 | |
998 return getAttribute(aria_valuetextAttr).string(); | |
999 } | |
1000 | |
1001 float AXNodeObject::valueForRange() const | |
1002 { | |
1003 if (hasAttribute(aria_valuenowAttr)) | |
1004 return getAttribute(aria_valuenowAttr).toFloat(); | |
1005 | |
1006 if (isHTMLInputElement(node())) { | |
1007 HTMLInputElement& input = toHTMLInputElement(*node()); | |
1008 if (input.type() == InputTypeNames::range) | |
1009 return input.valueAsNumber(); | |
1010 } | |
1011 | |
1012 return 0.0; | |
1013 } | |
1014 | |
1015 float AXNodeObject::maxValueForRange() const | |
1016 { | |
1017 if (hasAttribute(aria_valuemaxAttr)) | |
1018 return getAttribute(aria_valuemaxAttr).toFloat(); | |
1019 | |
1020 if (isHTMLInputElement(node())) { | |
1021 HTMLInputElement& input = toHTMLInputElement(*node()); | |
1022 if (input.type() == InputTypeNames::range) | |
1023 return input.maximum(); | |
1024 } | |
1025 | |
1026 return 0.0; | |
1027 } | |
1028 | |
1029 float AXNodeObject::minValueForRange() const | |
1030 { | |
1031 if (hasAttribute(aria_valueminAttr)) | |
1032 return getAttribute(aria_valueminAttr).toFloat(); | |
1033 | |
1034 if (isHTMLInputElement(node())) { | |
1035 HTMLInputElement& input = toHTMLInputElement(*node()); | |
1036 if (input.type() == InputTypeNames::range) | |
1037 return input.minimum(); | |
1038 } | |
1039 | |
1040 return 0.0; | |
1041 } | |
1042 | |
1043 float AXNodeObject::stepValueForRange() const | |
1044 { | |
1045 return getAttribute(stepAttr).toFloat(); | |
1046 } | |
1047 | |
1048 String AXNodeObject::stringValue() const | |
1049 { | |
1050 Node* node = this->node(); | |
1051 if (!node) | |
1052 return String(); | |
1053 | |
1054 if (ariaRoleAttribute() == StaticTextRole) { | |
1055 String staticText = text(); | |
1056 if (!staticText.length()) | |
1057 staticText = textUnderElement(); | |
1058 return staticText; | |
1059 } | |
1060 | |
1061 if (node->isTextNode()) | |
1062 return textUnderElement(); | |
1063 | |
1064 if (isHTMLSelectElement(*node)) { | |
1065 HTMLSelectElement& selectElement = toHTMLSelectElement(*node); | |
1066 int selectedIndex = selectElement.selectedIndex(); | |
1067 const WillBeHeapVector<RawPtrWillBeMember<HTMLElement> >& listItems = se
lectElement.listItems(); | |
1068 if (selectedIndex >= 0 && static_cast<size_t>(selectedIndex) < listItems
.size()) { | |
1069 const AtomicString& overriddenDescription = listItems[selectedIndex]
->fastGetAttribute(aria_labelAttr); | |
1070 if (!overriddenDescription.isNull()) | |
1071 return overriddenDescription; | |
1072 } | |
1073 if (!selectElement.multiple()) | |
1074 return selectElement.value(); | |
1075 return String(); | |
1076 } | |
1077 | |
1078 if (isTextControl()) | |
1079 return text(); | |
1080 | |
1081 // FIXME: We might need to implement a value here for more types | |
1082 // FIXME: It would be better not to advertise a value at all for the types f
or which we don't implement one; | |
1083 // this would require subclassing or making accessibilityAttributeNames do s
omething other than return a | |
1084 // single static array. | |
1085 return String(); | |
1086 } | |
1087 | |
1088 | |
1089 const AtomicString& AXNodeObject::textInputType() const | |
1090 { | |
1091 Node* node = this->node(); | |
1092 if (!isHTMLInputElement(node)) | |
1093 return nullAtom; | |
1094 | |
1095 HTMLInputElement& input = toHTMLInputElement(*node); | |
1096 if (input.isTextField()) | |
1097 return input.type(); | |
1098 return nullAtom; | |
1099 } | |
1100 | |
1101 String AXNodeObject::ariaDescribedByAttribute() const | |
1102 { | |
1103 WillBeHeapVector<RawPtrWillBeMember<Element> > elements; | |
1104 elementsFromAttribute(elements, aria_describedbyAttr); | |
1105 | |
1106 return accessibilityDescriptionForElements(elements); | |
1107 } | |
1108 | |
1109 String AXNodeObject::ariaLabeledByAttribute() const | |
1110 { | |
1111 WillBeHeapVector<RawPtrWillBeMember<Element> > elements; | |
1112 ariaLabeledByElements(elements); | |
1113 | |
1114 return accessibilityDescriptionForElements(elements); | |
1115 } | |
1116 | |
1117 AccessibilityRole AXNodeObject::ariaRoleAttribute() const | |
1118 { | |
1119 return m_ariaRole; | |
1120 } | |
1121 | |
1122 // When building the textUnderElement for an object, determine whether or not | |
1123 // we should include the inner text of this given descendant object or skip it. | |
1124 static bool shouldUseAccessibilityObjectInnerText(AXObject* obj) | |
1125 { | |
1126 // Consider this hypothetical example: | |
1127 // <div tabindex=0> | |
1128 // <h2> | |
1129 // Table of contents | |
1130 // </h2> | |
1131 // <a href="#start">Jump to start of book</a> | |
1132 // <ul> | |
1133 // <li><a href="#1">Chapter 1</a></li> | |
1134 // <li><a href="#1">Chapter 2</a></li> | |
1135 // </ul> | |
1136 // </div> | |
1137 // | |
1138 // The goal is to return a reasonable title for the outer container div, bec
ause | |
1139 // it's focusable - but without making its title be the full inner text, whi
ch is | |
1140 // quite long. As a heuristic, skip links, controls, and elements that are u
sually | |
1141 // containers with lots of children. | |
1142 | |
1143 // Skip hidden children | |
1144 if (obj->isInertOrAriaHidden()) | |
1145 return false; | |
1146 | |
1147 // Skip focusable children, so we don't include the text of links and contro
ls. | |
1148 if (obj->canSetFocusAttribute()) | |
1149 return false; | |
1150 | |
1151 // Skip big container elements like lists, tables, etc. | |
1152 if (obj->isList() || obj->isAXTable() || obj->isTree() || obj->isCanvas()) | |
1153 return false; | |
1154 | |
1155 return true; | |
1156 } | |
1157 | |
1158 String AXNodeObject::textUnderElement() const | |
1159 { | |
1160 Node* node = this->node(); | |
1161 if (node && node->isTextNode()) | |
1162 return toText(node)->wholeText(); | |
1163 | |
1164 StringBuilder builder; | |
1165 for (AXObject* child = firstChild(); child; child = child->nextSibling()) { | |
1166 if (!shouldUseAccessibilityObjectInnerText(child)) | |
1167 continue; | |
1168 | |
1169 if (child->isAXNodeObject()) { | |
1170 Vector<AccessibilityText> textOrder; | |
1171 toAXNodeObject(child)->alternativeText(textOrder); | |
1172 if (textOrder.size() > 0) { | |
1173 builder.append(textOrder[0].text); | |
1174 continue; | |
1175 } | |
1176 } | |
1177 | |
1178 builder.append(child->textUnderElement()); | |
1179 } | |
1180 | |
1181 return builder.toString(); | |
1182 } | |
1183 | |
1184 String AXNodeObject::accessibilityDescription() const | |
1185 { | |
1186 // Static text should not have a description, it should only have a stringVa
lue. | |
1187 if (roleValue() == StaticTextRole) | |
1188 return String(); | |
1189 | |
1190 String ariaDescription = ariaAccessibilityDescription(); | |
1191 if (!ariaDescription.isEmpty()) | |
1192 return ariaDescription; | |
1193 | |
1194 if (isImage() || isInputImage() || isNativeImage() || isCanvas()) { | |
1195 // Images should use alt as long as the attribute is present, even if em
pty. | |
1196 // Otherwise, it should fallback to other methods, like the title attrib
ute. | |
1197 const AtomicString& alt = getAttribute(altAttr); | |
1198 if (!alt.isNull()) | |
1199 return alt; | |
1200 } | |
1201 | |
1202 // An element's descriptive text is comprised of title() (what's visible on
the screen) and accessibilityDescription() (other descriptive text). | |
1203 // Both are used to generate what a screen reader speaks. | |
1204 // If this point is reached (i.e. there's no accessibilityDescription) and t
here's no title(), we should fallback to using the title attribute. | |
1205 // The title attribute is normally used as help text (because it is a toolti
p), but if there is nothing else available, this should be used (according to AR
IA). | |
1206 if (title().isEmpty()) | |
1207 return getAttribute(titleAttr); | |
1208 | |
1209 return String(); | |
1210 } | |
1211 | |
1212 String AXNodeObject::title() const | |
1213 { | |
1214 Node* node = this->node(); | |
1215 if (!node) | |
1216 return String(); | |
1217 | |
1218 bool isInputElement = isHTMLInputElement(*node); | |
1219 if (isInputElement) { | |
1220 HTMLInputElement& input = toHTMLInputElement(*node); | |
1221 if (input.isTextButton()) | |
1222 return input.valueWithDefault(); | |
1223 } | |
1224 | |
1225 if (isInputElement || AXObject::isARIAInput(ariaRoleAttribute()) || isContro
l()) { | |
1226 HTMLLabelElement* label = labelForElement(toElement(node)); | |
1227 if (label && !exposesTitleUIElement()) | |
1228 return label->innerText(); | |
1229 } | |
1230 | |
1231 // If this node isn't rendered, there's no inner text we can extract from a
select element. | |
1232 if (!isAXRenderObject() && isHTMLSelectElement(*node)) | |
1233 return String(); | |
1234 | |
1235 switch (roleValue()) { | |
1236 case PopUpButtonRole: | |
1237 // Native popup buttons should not use their button children's text as a
title. That value is retrieved through stringValue(). | |
1238 if (isHTMLSelectElement(*node)) | |
1239 return String(); | |
1240 case ButtonRole: | |
1241 case ToggleButtonRole: | |
1242 case CheckBoxRole: | |
1243 case ListBoxOptionRole: | |
1244 case MenuButtonRole: | |
1245 case MenuItemRole: | |
1246 case MenuItemCheckBoxRole: | |
1247 case MenuItemRadioRole: | |
1248 case RadioButtonRole: | |
1249 case TabRole: | |
1250 return textUnderElement(); | |
1251 // SVGRoots should not use the text under itself as a title. That could incl
ude the text of objects like <text>. | |
1252 case SVGRootRole: | |
1253 return String(); | |
1254 default: | |
1255 break; | |
1256 } | |
1257 | |
1258 if (isHeading() || isLink()) | |
1259 return textUnderElement(); | |
1260 | |
1261 // If it's focusable but it's not content editable or a known control type,
then it will appear to | |
1262 // the user as a single atomic object, so we should use its text as the defa
ult title. | |
1263 if (isGenericFocusableElement()) | |
1264 return textUnderElement(); | |
1265 | |
1266 return String(); | |
1267 } | |
1268 | |
1269 String AXNodeObject::helpText() const | |
1270 { | |
1271 Node* node = this->node(); | |
1272 if (!node) | |
1273 return String(); | |
1274 | |
1275 const AtomicString& ariaHelp = getAttribute(aria_helpAttr); | |
1276 if (!ariaHelp.isEmpty()) | |
1277 return ariaHelp; | |
1278 | |
1279 String describedBy = ariaDescribedByAttribute(); | |
1280 if (!describedBy.isEmpty()) | |
1281 return describedBy; | |
1282 | |
1283 String description = accessibilityDescription(); | |
1284 for (Node* curr = node; curr; curr = curr->parentNode()) { | |
1285 if (curr->isHTMLElement()) { | |
1286 const AtomicString& summary = toElement(curr)->getAttribute(summaryA
ttr); | |
1287 if (!summary.isEmpty()) | |
1288 return summary; | |
1289 | |
1290 // The title attribute should be used as help text unless it is alre
ady being used as descriptive text. | |
1291 const AtomicString& title = toElement(curr)->getAttribute(titleAttr)
; | |
1292 if (!title.isEmpty() && description != title) | |
1293 return title; | |
1294 } | |
1295 | |
1296 // Only take help text from an ancestor element if its a group or an unk
nown role. If help was | |
1297 // added to those kinds of elements, it is likely it was meant for a chi
ld element. | |
1298 AXObject* axObj = axObjectCache()->getOrCreate(curr); | |
1299 if (axObj) { | |
1300 AccessibilityRole role = axObj->roleValue(); | |
1301 if (role != GroupRole && role != UnknownRole) | |
1302 break; | |
1303 } | |
1304 } | |
1305 | |
1306 return String(); | |
1307 } | |
1308 | |
1309 LayoutRect AXNodeObject::elementRect() const | |
1310 { | |
1311 // First check if it has a custom rect, for example if this element is tied
to a canvas path. | |
1312 if (!m_explicitElementRect.isEmpty()) | |
1313 return m_explicitElementRect; | |
1314 | |
1315 // FIXME: If there are a lot of elements in the canvas, it will be inefficie
nt. | |
1316 // We can avoid the inefficient calculations by using AXComputedObjectAttrib
uteCache. | |
1317 if (node()->parentElement()->isInCanvasSubtree()) { | |
1318 LayoutRect rect; | |
1319 | |
1320 for (Node* child = node()->firstChild(); child; child = child->nextSibli
ng()) { | |
1321 if (child->isHTMLElement()) { | |
1322 if (AXObject* obj = axObjectCache()->get(child)) { | |
1323 if (rect.isEmpty()) | |
1324 rect = obj->elementRect(); | |
1325 else | |
1326 rect.unite(obj->elementRect()); | |
1327 } | |
1328 } | |
1329 } | |
1330 | |
1331 if (!rect.isEmpty()) | |
1332 return rect; | |
1333 } | |
1334 | |
1335 // If this object doesn't have an explicit element rect or computable from i
ts children, | |
1336 // for now, let's return the position of the ancestor that does have a posit
ion, | |
1337 // and make it the width of that parent, and about the height of a line of t
ext, so that it's clear the object is a child of the parent. | |
1338 | |
1339 LayoutRect boundingBox; | |
1340 | |
1341 for (AXObject* positionProvider = parentObject(); positionProvider; position
Provider = positionProvider->parentObject()) { | |
1342 if (positionProvider->isAXRenderObject()) { | |
1343 LayoutRect parentRect = positionProvider->elementRect(); | |
1344 boundingBox.setSize(LayoutSize(parentRect.width(), LayoutUnit(std::m
in(10.0f, parentRect.height().toFloat())))); | |
1345 boundingBox.setLocation(parentRect.location()); | |
1346 break; | |
1347 } | |
1348 } | |
1349 | |
1350 return boundingBox; | |
1351 } | |
1352 | |
1353 AXObject* AXNodeObject::computeParent() const | |
1354 { | |
1355 if (!node()) | |
1356 return 0; | |
1357 | |
1358 Node* parentObj = node()->parentNode(); | |
1359 if (parentObj) | |
1360 return axObjectCache()->getOrCreate(parentObj); | |
1361 | |
1362 return 0; | |
1363 } | |
1364 | |
1365 AXObject* AXNodeObject::computeParentIfExists() const | |
1366 { | |
1367 if (!node()) | |
1368 return 0; | |
1369 | |
1370 Node* parentObj = node()->parentNode(); | |
1371 if (parentObj) | |
1372 return axObjectCache()->get(parentObj); | |
1373 | |
1374 return 0; | |
1375 } | |
1376 | |
1377 AXObject* AXNodeObject::firstChild() const | |
1378 { | |
1379 if (!node()) | |
1380 return 0; | |
1381 | |
1382 Node* firstChild = node()->firstChild(); | |
1383 | |
1384 if (!firstChild) | |
1385 return 0; | |
1386 | |
1387 return axObjectCache()->getOrCreate(firstChild); | |
1388 } | |
1389 | |
1390 AXObject* AXNodeObject::nextSibling() const | |
1391 { | |
1392 if (!node()) | |
1393 return 0; | |
1394 | |
1395 Node* nextSibling = node()->nextSibling(); | |
1396 if (!nextSibling) | |
1397 return 0; | |
1398 | |
1399 return axObjectCache()->getOrCreate(nextSibling); | |
1400 } | |
1401 | |
1402 void AXNodeObject::addChildren() | |
1403 { | |
1404 // If the need to add more children in addition to existing children arises, | |
1405 // childrenChanged should have been called, leaving the object with no child
ren. | |
1406 ASSERT(!m_haveChildren); | |
1407 | |
1408 if (!m_node) | |
1409 return; | |
1410 | |
1411 m_haveChildren = true; | |
1412 | |
1413 // The only time we add children from the DOM tree to a node with a renderer
is when it's a canvas. | |
1414 if (renderer() && !isHTMLCanvasElement(*m_node)) | |
1415 return; | |
1416 | |
1417 for (Node* child = m_node->firstChild(); child; child = child->nextSibling()
) | |
1418 addChild(axObjectCache()->getOrCreate(child)); | |
1419 | |
1420 for (unsigned i = 0; i < m_children.size(); ++i) | |
1421 m_children[i].get()->setParent(this); | |
1422 } | |
1423 | |
1424 void AXNodeObject::addChild(AXObject* child) | |
1425 { | |
1426 insertChild(child, m_children.size()); | |
1427 } | |
1428 | |
1429 void AXNodeObject::insertChild(AXObject* child, unsigned index) | |
1430 { | |
1431 if (!child) | |
1432 return; | |
1433 | |
1434 // If the parent is asking for this child's children, then either it's the f
irst time (and clearing is a no-op), | |
1435 // or its visibility has changed. In the latter case, this child may have a
stale child cached. | |
1436 // This can prevent aria-hidden changes from working correctly. Hence, whene
ver a parent is getting children, ensure data is not stale. | |
1437 child->clearChildren(); | |
1438 | |
1439 if (child->accessibilityIsIgnored()) { | |
1440 AccessibilityChildrenVector children = child->children(); | |
1441 size_t length = children.size(); | |
1442 for (size_t i = 0; i < length; ++i) | |
1443 m_children.insert(index + i, children[i]); | |
1444 } else { | |
1445 ASSERT(child->parentObject() == this); | |
1446 m_children.insert(index, child); | |
1447 } | |
1448 } | |
1449 | |
1450 bool AXNodeObject::canHaveChildren() const | |
1451 { | |
1452 // If this is an AXRenderObject, then it's okay if this object | |
1453 // doesn't have a node - there are some renderers that don't have associated | |
1454 // nodes, like scroll areas and css-generated text. | |
1455 if (!node() && !isAXRenderObject()) | |
1456 return false; | |
1457 | |
1458 // Elements that should not have children | |
1459 switch (roleValue()) { | |
1460 case ImageRole: | |
1461 case ButtonRole: | |
1462 case PopUpButtonRole: | |
1463 case CheckBoxRole: | |
1464 case RadioButtonRole: | |
1465 case TabRole: | |
1466 case ToggleButtonRole: | |
1467 case ListBoxOptionRole: | |
1468 case ScrollBarRole: | |
1469 return false; | |
1470 case StaticTextRole: | |
1471 if (!axObjectCache()->inlineTextBoxAccessibilityEnabled()) | |
1472 return false; | |
1473 default: | |
1474 return true; | |
1475 } | |
1476 } | |
1477 | |
1478 Element* AXNodeObject::actionElement() const | |
1479 { | |
1480 Node* node = this->node(); | |
1481 if (!node) | |
1482 return 0; | |
1483 | |
1484 if (isHTMLInputElement(*node)) { | |
1485 HTMLInputElement& input = toHTMLInputElement(*node); | |
1486 if (!input.isDisabledFormControl() && (isCheckboxOrRadio() || input.isTe
xtButton())) | |
1487 return &input; | |
1488 } else if (isHTMLButtonElement(*node)) { | |
1489 return toElement(node); | |
1490 } | |
1491 | |
1492 if (isFileUploadButton()) | |
1493 return toElement(node); | |
1494 | |
1495 if (AXObject::isARIAInput(ariaRoleAttribute())) | |
1496 return toElement(node); | |
1497 | |
1498 if (isImageButton()) | |
1499 return toElement(node); | |
1500 | |
1501 if (isHTMLSelectElement(*node)) | |
1502 return toElement(node); | |
1503 | |
1504 switch (roleValue()) { | |
1505 case ButtonRole: | |
1506 case PopUpButtonRole: | |
1507 case ToggleButtonRole: | |
1508 case TabRole: | |
1509 case MenuItemRole: | |
1510 case MenuItemCheckBoxRole: | |
1511 case MenuItemRadioRole: | |
1512 case ListItemRole: | |
1513 return toElement(node); | |
1514 default: | |
1515 break; | |
1516 } | |
1517 | |
1518 Element* elt = anchorElement(); | |
1519 if (!elt) | |
1520 elt = mouseButtonListener(); | |
1521 return elt; | |
1522 } | |
1523 | |
1524 Element* AXNodeObject::anchorElement() const | |
1525 { | |
1526 Node* node = this->node(); | |
1527 if (!node) | |
1528 return 0; | |
1529 | |
1530 AXObjectCacheImpl* cache = axObjectCache(); | |
1531 | |
1532 // search up the DOM tree for an anchor element | |
1533 // NOTE: this assumes that any non-image with an anchor is an HTMLAnchorElem
ent | |
1534 for ( ; node; node = node->parentNode()) { | |
1535 if (isHTMLAnchorElement(*node) || (node->renderer() && cache->getOrCreat
e(node->renderer())->isAnchor())) | |
1536 return toElement(node); | |
1537 } | |
1538 | |
1539 return 0; | |
1540 } | |
1541 | |
1542 Document* AXNodeObject::document() const | |
1543 { | |
1544 if (!node()) | |
1545 return 0; | |
1546 return &node()->document(); | |
1547 } | |
1548 | |
1549 void AXNodeObject::setNode(Node* node) | |
1550 { | |
1551 m_node = node; | |
1552 } | |
1553 | |
1554 AXObject* AXNodeObject::correspondingControlForLabelElement() const | |
1555 { | |
1556 HTMLLabelElement* labelElement = labelElementContainer(); | |
1557 if (!labelElement) | |
1558 return 0; | |
1559 | |
1560 HTMLElement* correspondingControl = labelElement->control(); | |
1561 if (!correspondingControl) | |
1562 return 0; | |
1563 | |
1564 // Make sure the corresponding control isn't a descendant of this label | |
1565 // that's in the middle of being destroyed. | |
1566 if (correspondingControl->renderer() && !correspondingControl->renderer()->p
arent()) | |
1567 return 0; | |
1568 | |
1569 return axObjectCache()->getOrCreate(correspondingControl); | |
1570 } | |
1571 | |
1572 HTMLLabelElement* AXNodeObject::labelElementContainer() const | |
1573 { | |
1574 if (!node()) | |
1575 return 0; | |
1576 | |
1577 // the control element should not be considered part of the label | |
1578 if (isControl()) | |
1579 return 0; | |
1580 | |
1581 // find if this has a ancestor that is a label | |
1582 return Traversal<HTMLLabelElement>::firstAncestorOrSelf(*node()); | |
1583 } | |
1584 | |
1585 void AXNodeObject::setFocused(bool on) | |
1586 { | |
1587 if (!canSetFocusAttribute()) | |
1588 return; | |
1589 | |
1590 Document* document = this->document(); | |
1591 if (!on) { | |
1592 document->setFocusedElement(nullptr); | |
1593 } else { | |
1594 Node* node = this->node(); | |
1595 if (node && node->isElementNode()) { | |
1596 // If this node is already the currently focused node, then calling
focus() won't do anything. | |
1597 // That is a problem when focus is removed from the webpage to chrom
e, and then returns. | |
1598 // In these cases, we need to do what keyboard and mouse focus do, w
hich is reset focus first. | |
1599 if (document->focusedElement() == node) | |
1600 document->setFocusedElement(nullptr); | |
1601 | |
1602 toElement(node)->focus(); | |
1603 } else { | |
1604 document->setFocusedElement(nullptr); | |
1605 } | |
1606 } | |
1607 } | |
1608 | |
1609 void AXNodeObject::increment() | |
1610 { | |
1611 UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture); | |
1612 alterSliderValue(true); | |
1613 } | |
1614 | |
1615 void AXNodeObject::decrement() | |
1616 { | |
1617 UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture); | |
1618 alterSliderValue(false); | |
1619 } | |
1620 | |
1621 void AXNodeObject::childrenChanged() | |
1622 { | |
1623 // This method is meant as a quick way of marking a portion of the accessibi
lity tree dirty. | |
1624 if (!node() && !renderer()) | |
1625 return; | |
1626 | |
1627 axObjectCache()->postNotification(this, document(), AXObjectCacheImpl::AXChi
ldrenChanged, true); | |
1628 | |
1629 // Go up the accessibility parent chain, but only if the element already exi
sts. This method is | |
1630 // called during render layouts, minimal work should be done. | |
1631 // If AX elements are created now, they could interrogate the render tree wh
ile it's in a funky state. | |
1632 // At the same time, process ARIA live region changes. | |
1633 for (AXObject* parent = this; parent; parent = parent->parentObjectIfExists(
)) { | |
1634 parent->setNeedsToUpdateChildren(); | |
1635 | |
1636 // These notifications always need to be sent because screenreaders are
reliant on them to perform. | |
1637 // In other words, they need to be sent even when the screen reader has
not accessed this live region since the last update. | |
1638 | |
1639 // If this element supports ARIA live regions, then notify the AT of cha
nges. | |
1640 if (parent->isLiveRegion()) | |
1641 axObjectCache()->postNotification(parent, parent->document(), AXObje
ctCacheImpl::AXLiveRegionChanged, true); | |
1642 | |
1643 // If this element is an ARIA text box or content editable, post a "valu
e changed" notification on it | |
1644 // so that it behaves just like a native input element or textarea. | |
1645 if (isNonNativeTextControl()) | |
1646 axObjectCache()->postNotification(parent, parent->document(), AXObje
ctCacheImpl::AXValueChanged, true); | |
1647 } | |
1648 } | |
1649 | |
1650 void AXNodeObject::selectionChanged() | |
1651 { | |
1652 // Post the selected text changed event on the first ancestor that's | |
1653 // focused (to handle form controls, ARIA text boxes and contentEditable), | |
1654 // or the web area if the selection is just in the document somewhere. | |
1655 if (isFocused() || isWebArea()) | |
1656 axObjectCache()->postNotification(this, document(), AXObjectCacheImpl::A
XSelectedTextChanged, true); | |
1657 else | |
1658 AXObject::selectionChanged(); // Calls selectionChanged on parent. | |
1659 } | |
1660 | |
1661 void AXNodeObject::textChanged() | |
1662 { | |
1663 // If this element supports ARIA live regions, or is part of a region with a
n ARIA editable role, | |
1664 // then notify the AT of changes. | |
1665 AXObjectCacheImpl* cache = axObjectCache(); | |
1666 for (Node* parentNode = node(); parentNode; parentNode = parentNode->parentN
ode()) { | |
1667 AXObject* parent = cache->get(parentNode); | |
1668 if (!parent) | |
1669 continue; | |
1670 | |
1671 if (parent->isLiveRegion()) | |
1672 cache->postNotification(parentNode, AXObjectCacheImpl::AXLiveRegionC
hanged, true); | |
1673 | |
1674 // If this element is an ARIA text box or content editable, post a "valu
e changed" notification on it | |
1675 // so that it behaves just like a native input element or textarea. | |
1676 if (parent->isNonNativeTextControl()) | |
1677 cache->postNotification(parentNode, AXObjectCacheImpl::AXValueChange
d, true); | |
1678 } | |
1679 } | |
1680 | |
1681 void AXNodeObject::updateAccessibilityRole() | |
1682 { | |
1683 bool ignoredStatus = accessibilityIsIgnored(); | |
1684 m_role = determineAccessibilityRole(); | |
1685 | |
1686 // The AX hierarchy only needs to be updated if the ignored status of an ele
ment has changed. | |
1687 if (ignoredStatus != accessibilityIsIgnored()) | |
1688 childrenChanged(); | |
1689 } | |
1690 | |
1691 String AXNodeObject::alternativeTextForWebArea() const | |
1692 { | |
1693 // The WebArea description should follow this order: | |
1694 // aria-label on the <html> | |
1695 // title on the <html> | |
1696 // <title> inside the <head> (of it was set through JS) | |
1697 // name on the <html> | |
1698 // For iframes: | |
1699 // aria-label on the <iframe> | |
1700 // title on the <iframe> | |
1701 // name on the <iframe> | |
1702 | |
1703 Document* document = this->document(); | |
1704 if (!document) | |
1705 return String(); | |
1706 | |
1707 // Check if the HTML element has an aria-label for the webpage. | |
1708 if (Element* documentElement = document->documentElement()) { | |
1709 const AtomicString& ariaLabel = documentElement->getAttribute(aria_label
Attr); | |
1710 if (!ariaLabel.isEmpty()) | |
1711 return ariaLabel; | |
1712 } | |
1713 | |
1714 if (HTMLFrameOwnerElement* owner = document->ownerElement()) { | |
1715 if (isHTMLFrameElementBase(*owner)) { | |
1716 const AtomicString& title = owner->getAttribute(titleAttr); | |
1717 if (!title.isEmpty()) | |
1718 return title; | |
1719 } | |
1720 return owner->getNameAttribute(); | |
1721 } | |
1722 | |
1723 String documentTitle = document->title(); | |
1724 if (!documentTitle.isEmpty()) | |
1725 return documentTitle; | |
1726 | |
1727 if (HTMLElement* body = document->body()) | |
1728 return body->getNameAttribute(); | |
1729 | |
1730 return String(); | |
1731 } | |
1732 | |
1733 void AXNodeObject::alternativeText(Vector<AccessibilityText>& textOrder) const | |
1734 { | |
1735 if (isWebArea()) { | |
1736 String webAreaText = alternativeTextForWebArea(); | |
1737 if (!webAreaText.isEmpty()) | |
1738 textOrder.append(AccessibilityText(webAreaText, AlternativeText)); | |
1739 return; | |
1740 } | |
1741 | |
1742 ariaLabeledByText(textOrder); | |
1743 | |
1744 const AtomicString& ariaLabel = getAttribute(aria_labelAttr); | |
1745 if (!ariaLabel.isEmpty()) | |
1746 textOrder.append(AccessibilityText(ariaLabel, AlternativeText)); | |
1747 | |
1748 if (isImage() || isInputImage() || isNativeImage() || isCanvas()) { | |
1749 // Images should use alt as long as the attribute is present, even if em
pty. | |
1750 // Otherwise, it should fallback to other methods, like the title attrib
ute. | |
1751 const AtomicString& alt = getAttribute(altAttr); | |
1752 if (!alt.isNull()) | |
1753 textOrder.append(AccessibilityText(alt, AlternativeText)); | |
1754 } | |
1755 } | |
1756 | |
1757 void AXNodeObject::ariaLabeledByText(Vector<AccessibilityText>& textOrder) const | |
1758 { | |
1759 String ariaLabeledBy = ariaLabeledByAttribute(); | |
1760 if (!ariaLabeledBy.isEmpty()) { | |
1761 WillBeHeapVector<RawPtrWillBeMember<Element> > elements; | |
1762 ariaLabeledByElements(elements); | |
1763 | |
1764 unsigned length = elements.size(); | |
1765 for (unsigned k = 0; k < length; k++) { | |
1766 RefPtr<AXObject> axElement = axObjectCache()->getOrCreate(elements[k
]); | |
1767 textOrder.append(AccessibilityText(ariaLabeledBy, AlternativeText, a
xElement)); | |
1768 } | |
1769 } | |
1770 } | |
1771 | |
1772 void AXNodeObject::changeValueByPercent(float percentChange) | |
1773 { | |
1774 float range = maxValueForRange() - minValueForRange(); | |
1775 float value = valueForRange(); | |
1776 | |
1777 value += range * (percentChange / 100); | |
1778 setValue(String::number(value)); | |
1779 | |
1780 axObjectCache()->postNotification(node(), AXObjectCacheImpl::AXValueChanged,
true); | |
1781 } | |
1782 | |
1783 } // namespace blink | |
OLD | NEW |