diff options
Diffstat (limited to 'WebCore/accessibility/AccessibilityRenderObject.cpp')
-rw-r--r-- | WebCore/accessibility/AccessibilityRenderObject.cpp | 761 |
1 files changed, 562 insertions, 199 deletions
diff --git a/WebCore/accessibility/AccessibilityRenderObject.cpp b/WebCore/accessibility/AccessibilityRenderObject.cpp index 4c50b9a..c57dd92 100644 --- a/WebCore/accessibility/AccessibilityRenderObject.cpp +++ b/WebCore/accessibility/AccessibilityRenderObject.cpp @@ -30,8 +30,8 @@ #include "AccessibilityRenderObject.h" #include "AXObjectCache.h" -#include "AccessibilityListBox.h" #include "AccessibilityImageMapLink.h" +#include "AccessibilityListBox.h" #include "CharacterNames.h" #include "EventNames.h" #include "FloatRect.h" @@ -84,6 +84,7 @@ AccessibilityRenderObject::AccessibilityRenderObject(RenderObject* renderer) : AccessibilityObject() , m_renderer(renderer) , m_ariaRole(UnknownRole) + , m_childrenDirty(false) { updateAccessibilityRole(); #ifndef NDEBUG @@ -166,7 +167,7 @@ AccessibilityObject* AccessibilityRenderObject::parentObjectIfExists() const if (!m_renderer) return 0; - RenderObject *parent = m_renderer->parent(); + RenderObject* parent = m_renderer->parent(); if (!parent) return 0; @@ -178,7 +179,7 @@ AccessibilityObject* AccessibilityRenderObject::parentObject() const if (!m_renderer) return 0; - RenderObject *parent = m_renderer->parent(); + RenderObject* parent = m_renderer->parent(); if (!parent) return 0; @@ -296,10 +297,10 @@ bool AccessibilityRenderObject::isSlider() const bool AccessibilityRenderObject::isMenuRelated() const { AccessibilityRole role = roleValue(); - return role == MenuRole || - role == MenuBarRole || - role == MenuButtonRole || - role == MenuItemRole; + return role == MenuRole + || role == MenuBarRole + || role == MenuButtonRole + || role == MenuItemRole; } bool AccessibilityRenderObject::isMenu() const @@ -505,6 +506,24 @@ AccessibilityObject* AccessibilityRenderObject::selectedRadioButton() } return 0; } + +AccessibilityObject* AccessibilityRenderObject::selectedTabItem() +{ + if (!isTabList()) + return 0; + + // Find the child tab item that is selected (ie. the intValue == 1). + AccessibilityObject::AccessibilityChildrenVector tabs; + tabChildren(tabs); + + int count = tabs.size(); + for (int i = 0; i < count; ++i) { + AccessibilityObject* object = m_children[i].get(); + if (object->isTabItem() && object->intValue() == 1) + return object; + } + return 0; +} const AtomicString& AccessibilityRenderObject::getAttribute(const QualifiedName& attribute) const { @@ -693,6 +712,40 @@ String AccessibilityRenderObject::helpText() const return String(); } +unsigned AccessibilityRenderObject::hierarchicalLevel() const +{ + if (!m_renderer) + return 0; + + Node* node = m_renderer->node(); + if (!node || !node->isElementNode()) + return 0; + Element* element = static_cast<Element*>(node); + String ariaLevel = element->getAttribute(aria_levelAttr); + if (!ariaLevel.isEmpty()) + return ariaLevel.toInt(); + + // Only tree item will calculate its level through the DOM currently. + if (roleValue() != TreeItemRole) + return 0; + + // Hierarchy leveling starts at 0. + // We measure tree hierarchy by the number of groups that the item is within. + unsigned level = 0; + AccessibilityObject* parent = parentObject(); + while (parent) { + AccessibilityRole parentRole = parent->roleValue(); + if (parentRole == GroupRole) + level++; + else if (parentRole == TreeRole) + break; + + parent = parent->parentObject(); + } + + return level; +} + String AccessibilityRenderObject::language() const { if (!m_renderer) @@ -780,7 +833,7 @@ String AccessibilityRenderObject::valueDescription() const float AccessibilityRenderObject::valueForRange() const { - if (!isProgressIndicator() && !isSlider()) + if (!isProgressIndicator() && !isSlider() && !isScrollbar()) return 0.0f; return getAttribute(aria_valuenowAttr).toFloat(); @@ -807,6 +860,9 @@ String AccessibilityRenderObject::stringValue() const if (!m_renderer || isPasswordField()) return String(); + if (ariaRoleAttribute() == StaticTextRole) + return text(); + if (m_renderer->isText()) return textUnderElement(); @@ -864,56 +920,67 @@ static String accessibleNameForNode(Node* node) return String(); } -String AccessibilityRenderObject::ariaAccessibilityName(const String& s) const +String AccessibilityRenderObject::accessibilityDescriptionForElements(Vector<Element*> &elements) const { + Vector<UChar> ariaLabel; + unsigned size = elements.size(); + for (unsigned i = 0; i < size; ++i) { + Element* idElement = elements[i]; + + String nameFragment = accessibleNameForNode(idElement); + ariaLabel.append(nameFragment.characters(), nameFragment.length()); + for (Node* n = idElement->firstChild(); n; n = n->traverseNextNode(idElement)) { + nameFragment = accessibleNameForNode(n); + ariaLabel.append(nameFragment.characters(), nameFragment.length()); + } + + if (i != size - 1) + ariaLabel.append(' '); + } + return String::adopt(ariaLabel); +} + + +void AccessibilityRenderObject::elementsFromAttribute(Vector<Element*>& elements, const QualifiedName& attribute) const +{ + Node* node = m_renderer->node(); + if (!node || !node->isElementNode()) + return; + Document* document = m_renderer->document(); if (!document) - return String(); - - String idList = s; + return; + + String idList = getAttribute(attribute).string(); + if (idList.isEmpty()) + return; + idList.replace('\n', ' '); Vector<String> idVector; idList.split(' ', idVector); - - Vector<UChar> ariaLabel; + unsigned size = idVector.size(); for (unsigned i = 0; i < size; ++i) { String idName = idVector[i]; Element* idElement = document->getElementById(idName); - if (idElement) { - String nameFragment = accessibleNameForNode(idElement); - ariaLabel.append(nameFragment.characters(), nameFragment.length()); - for (Node* n = idElement->firstChild(); n; n = n->traverseNextNode(idElement)) { - nameFragment = accessibleNameForNode(n); - ariaLabel.append(nameFragment.characters(), nameFragment.length()); - } - - if (i != size - 1) - ariaLabel.append(' '); - } + if (idElement) + elements.append(idElement); } - return String::adopt(ariaLabel); } - + +void AccessibilityRenderObject::ariaLabeledByElements(Vector<Element*>& elements) const +{ + elementsFromAttribute(elements, aria_labeledbyAttr); + if (!elements.size()) + elementsFromAttribute(elements, aria_labelledbyAttr); +} + String AccessibilityRenderObject::ariaLabeledByAttribute() const { - Node* node = m_renderer->node(); - if (!node) - return String(); - - if (!node->isElementNode()) - return String(); - - // The ARIA spec uses the British spelling: "labelled." It seems prudent to support the American - // spelling ("labeled") as well. - String idList = getAttribute(aria_labeledbyAttr).string(); - if (idList.isEmpty()) { - idList = getAttribute(aria_labelledbyAttr).string(); - if (idList.isEmpty()) - return String(); - } - - return ariaAccessibilityName(idList); + Vector<Element*> elements; + ariaLabeledByElements(elements); + + return accessibilityDescriptionForElements(elements); } static HTMLLabelElement* labelForElement(Element* element) @@ -990,6 +1057,8 @@ String AccessibilityRenderObject::title() const || ariaRole == MenuItemRole || ariaRole == MenuButtonRole || ariaRole == RadioButtonRole + || ariaRole == CheckBoxRole + || ariaRole == TabRole || isHeading()) return textUnderElement(); @@ -1001,11 +1070,10 @@ String AccessibilityRenderObject::title() const String AccessibilityRenderObject::ariaDescribedByAttribute() const { - String idList = getAttribute(aria_describedbyAttr).string(); - if (idList.isEmpty()) - return String(); + Vector<Element*> elements; + elementsFromAttribute(elements, aria_describedbyAttr); - return ariaAccessibilityName(idList); + return accessibilityDescriptionForElements(elements); } String AccessibilityRenderObject::accessibilityDescription() const @@ -1032,7 +1100,7 @@ String AccessibilityRenderObject::accessibilityDescription() const } if (isWebArea()) { - Document *document = m_renderer->document(); + Document* document = m_renderer->document(); Node* owner = document->ownerElement(); if (owner) { if (owner->hasTagName(frameTag) || owner->hasTagName(iframeTag)) { @@ -1048,12 +1116,7 @@ String AccessibilityRenderObject::accessibilityDescription() const if (owner && owner->isHTMLElement()) return static_cast<HTMLElement*>(owner)->getAttribute(nameAttr); } - - if (roleValue() == DefinitionListTermRole) - return AXDefinitionListTermText(); - if (roleValue() == DefinitionListDefinitionRole) - return AXDefinitionListDefinitionText(); - + return String(); } @@ -1198,6 +1261,8 @@ void AccessibilityRenderObject::addRadioButtonGroupMembers(AccessibilityChildren // or an internal anchor connection void AccessibilityRenderObject::linkedUIElements(AccessibilityChildrenVector& linkedUIElements) const { + ariaFlowToElements(linkedUIElements); + if (isAnchor()) { AccessibilityObject* linkedAXElement = internalLinkElement(); if (linkedAXElement) @@ -1208,6 +1273,71 @@ void AccessibilityRenderObject::linkedUIElements(AccessibilityChildrenVector& li addRadioButtonGroupMembers(linkedUIElements); } +bool AccessibilityRenderObject::hasTextAlternative() const +{ + // ARIA: section 2A, bullet #3 says if aria-labeledby or aria-label appears, it should + // override the "label" element association. + if (!ariaLabeledByAttribute().isEmpty() || !getAttribute(aria_labelAttr).string().isEmpty()) + return true; + + return false; +} + +bool AccessibilityRenderObject::supportsARIAFlowTo() const +{ + return !getAttribute(aria_flowtoAttr).string().isEmpty(); +} + +void AccessibilityRenderObject::ariaFlowToElements(AccessibilityChildrenVector& flowTo) const +{ + Vector<Element*> elements; + elementsFromAttribute(elements, aria_flowtoAttr); + + AXObjectCache* cache = axObjectCache(); + unsigned count = elements.size(); + for (unsigned k = 0; k < count; ++k) { + Element* element = elements[k]; + AccessibilityObject* flowToElement = cache->getOrCreate(element->renderer()); + if (flowToElement) + flowTo.append(flowToElement); + } + +} + +bool AccessibilityRenderObject::supportsARIADropping() +{ + const AtomicString& dropEffect = getAttribute(aria_dropeffectAttr).string(); + return !dropEffect.isEmpty(); +} + +bool AccessibilityRenderObject::supportsARIADragging() +{ + const AtomicString& grabbed = getAttribute(aria_grabbedAttr).string(); + return equalIgnoringCase(grabbed, "true") || equalIgnoringCase(grabbed, "false"); +} + +bool AccessibilityRenderObject::isARIAGrabbed() +{ + return elementAttributeValue(aria_grabbedAttr); +} + +void AccessibilityRenderObject::setARIAGrabbed(bool grabbed) +{ + setElementAttributeValue(aria_grabbedAttr, grabbed); +} + +void AccessibilityRenderObject::determineARIADropEffects(Vector<String>& effects) +{ + String dropEffects = getAttribute(aria_dropeffectAttr).string(); + if (dropEffects.isEmpty()) { + effects.clear(); + return; + } + + dropEffects.replace('\n', ' '); + dropEffects.split(' ', effects); +} + bool AccessibilityRenderObject::exposesTitleUIElement() const { if (!isControl()) @@ -1217,6 +1347,9 @@ bool AccessibilityRenderObject::exposesTitleUIElement() const if (isCheckboxOrRadio() && getAttribute(titleAttr).isEmpty()) return false; + if (hasTextAlternative()) + return false; + return true; } @@ -1256,9 +1389,19 @@ bool AccessibilityRenderObject::ariaIsHidden() const return false; } +bool AccessibilityRenderObject::isDescendantOfBarrenParent() const +{ + for (AccessibilityObject* object = parentObject(); object; object = object->parentObject()) { + if (!object->canHaveChildren()) + return true; + } + + return false; +} + bool AccessibilityRenderObject::accessibilityIsIgnored() const { - // is the platform is interested in this object? + // Is the platform interested in this object? AccessibilityObjectPlatformInclusion decision = accessibilityPlatformIncludesObject(); if (decision == IncludeObject) return false; @@ -1276,6 +1419,10 @@ bool AccessibilityRenderObject::accessibilityIsIgnored() const if (isPresentationalChildOfAriaRole()) return true; + // If this element is within a parent that cannot have children, it should not be exposed. + if (isDescendantOfBarrenParent()) + return true; + if (roleValue() == IgnoredRole) return true; @@ -1288,7 +1435,7 @@ bool AccessibilityRenderObject::accessibilityIsIgnored() const // find out if this element is inside of a label element. // if so, it may be ignored because it's the label for a checkbox or radio button AccessibilityObject* controlObject = correspondingControlForLabelElement(); - if (controlObject && !controlObject->exposesTitleUIElement()) + if (controlObject && !controlObject->exposesTitleUIElement() && controlObject->isCheckboxOrRadio()) return true; AccessibilityRole ariaRole = ariaRoleAttribute(); @@ -1300,8 +1447,8 @@ bool AccessibilityRenderObject::accessibilityIsIgnored() const // NOTE: BRs always have text boxes now, so the text box check here can be removed if (m_renderer->isText()) { // static text beneath MenuItems and MenuButtons are just reported along with the menu item, so it's ignored on an individual level - if (parentObjectUnignored()->ariaRoleAttribute() == MenuItemRole || - parentObjectUnignored()->ariaRoleAttribute() == MenuButtonRole) + if (parentObjectUnignored()->ariaRoleAttribute() == MenuItemRole + || parentObjectUnignored()->ariaRoleAttribute() == MenuButtonRole) return true; RenderText* renderText = toRenderText(m_renderer); if (m_renderer->isBR() || !renderText->firstTextBox()) @@ -1388,6 +1535,10 @@ int AccessibilityRenderObject::layoutCount() const String AccessibilityRenderObject::text() const { + // If this is a user defined static text, use the accessible name computation. + if (ariaRoleAttribute() == StaticTextRole) + return accessibilityDescription(); + if (!isTextControl() || isPasswordField()) return String(); @@ -1545,6 +1696,46 @@ bool AccessibilityRenderObject::isVisited() const return m_renderer->style()->pseudoState() == PseudoVisited; } +bool AccessibilityRenderObject::isExpanded() const +{ + if (equalIgnoringCase(getAttribute(aria_expandedAttr).string(), "true")) + return true; + + return false; +} + +void AccessibilityRenderObject::setElementAttributeValue(const QualifiedName& attributeName, bool value) +{ + if (!m_renderer) + return; + + Node* node = m_renderer->node(); + if (!node || !node->isElementNode()) + return; + + Element* element = static_cast<Element*>(node); + element->setAttribute(attributeName, (value) ? "true" : "false"); +} + +bool AccessibilityRenderObject::elementAttributeValue(const QualifiedName& attributeName) +{ + if (!m_renderer) + return false; + + return equalIgnoringCase(getAttribute(attributeName).string(), "true"); +} + +void AccessibilityRenderObject::setIsExpanded(bool isExpanded) +{ + // Combo boxes and tree items can be expanded (in different ways on different platforms). + // That action translates into setting the aria-expanded attribute to true. + AccessibilityRole role = roleValue(); + if (role != ComboBoxRole && role != TreeItemRole) + return; + + setElementAttributeValue(aria_expandedAttr, isExpanded); +} + bool AccessibilityRenderObject::isRequired() const { if (equalIgnoringCase(getAttribute(aria_requiredAttr).string(), "true")) @@ -1562,9 +1753,56 @@ bool AccessibilityRenderObject::isSelected() const if (!node) return false; + String ariaSelected = getAttribute(aria_selectedAttr).string(); + if (equalIgnoringCase(ariaSelected, "true")) + return true; + + if (isTabItem() && isTabItemSelected()) + return true; + return false; } +bool AccessibilityRenderObject::isTabItemSelected() const +{ + if (!isTabItem() || !m_renderer) + return false; + + Node* node = m_renderer->node(); + if (!node || !node->isElementNode()) + return false; + + // The ARIA spec says a tab item can also be selected if it is aria-labeled by a tabpanel + // that has keyboard focus inside of it, or if a tabpanel in its aria-controls list has KB + // focus inside of it. + AccessibilityObject* focusedElement = focusedUIElement(); + if (!focusedElement) + return false; + + Vector<Element*> elements; + elementsFromAttribute(elements, aria_controlsAttr); + + unsigned count = elements.size(); + for (unsigned k = 0; k < count; ++k) { + Element* element = elements[k]; + AccessibilityObject* tabPanel = axObjectCache()->getOrCreate(element->renderer()); + + // A tab item should only control tab panels. + if (!tabPanel || tabPanel->roleValue() != TabPanelRole) + continue; + + AccessibilityObject* checkFocusElement = focusedElement; + // Check if the focused element is a descendant of the element controlled by the tab item. + while (checkFocusElement) { + if (tabPanel == checkFocusElement) + return true; + checkFocusElement = checkFocusElement->parentObject(); + } + } + + return false; +} + bool AccessibilityRenderObject::isFocused() const { if (!m_renderer) @@ -1580,8 +1818,8 @@ bool AccessibilityRenderObject::isFocused() const // A web area is represented by the Document node in the DOM tree, which isn't focusable. // Check instead if the frame's selection controller is focused - if (focusedNode == m_renderer->node() || - (roleValue() == WebAreaRole && document->frame()->selection()->isFocusedAndActive())) + if (focusedNode == m_renderer->node() + || (roleValue() == WebAreaRole && document->frame()->selection()->isFocusedAndActive())) return true; return false; @@ -1613,6 +1851,26 @@ void AccessibilityRenderObject::changeValueByPercent(float percentChange) axObjectCache()->postNotification(m_renderer, AXObjectCache::AXValueChanged, true); } +void AccessibilityRenderObject::setSelected(bool enabled) +{ + setElementAttributeValue(aria_selectedAttr, enabled); +} + +void AccessibilityRenderObject::setSelectedRows(AccessibilityChildrenVector& selectedRows) +{ + // Setting selected rows only works on trees for now. + if (roleValue() != TreeRole) + return; + + bool isMultiselectable = elementAttributeValue(aria_multiselectableAttr); + unsigned count = selectedRows.size(); + if (count > 1 && !isMultiselectable) + count = 1; + + for (unsigned k = 0; k < count; ++k) + selectedRows[k]->setSelected(true); +} + void AccessibilityRenderObject::setValue(const String& string) { if (!m_renderer) @@ -1632,6 +1890,29 @@ void AccessibilityRenderObject::setValue(const String& string) } } +void AccessibilityRenderObject::ariaOwnsElements(AccessibilityChildrenVector& axObjects) const +{ + Vector<Element*> elements; + elementsFromAttribute(elements, aria_ownsAttr); + + unsigned count = elements.size(); + for (unsigned k = 0; k < count; ++k) { + RenderObject* render = elements[k]->renderer(); + AccessibilityObject* obj = axObjectCache()->getOrCreate(render); + if (obj) + axObjects.append(obj); + } +} + +bool AccessibilityRenderObject::supportsARIAOwns() const +{ + if (!m_renderer) + return false; + const AtomicString& ariaOwns = getAttribute(aria_ownsAttr).string(); + + return !ariaOwns.isEmpty(); +} + bool AccessibilityRenderObject::isEnabled() const { ASSERT(m_renderer); @@ -1777,7 +2058,7 @@ VisiblePositionRange AccessibilityRenderObject::visiblePositionRange() const VisiblePositionRange AccessibilityRenderObject::visiblePositionRangeForLine(unsigned lineCount) const { - if (lineCount == 0 || !m_renderer) + if (!lineCount || !m_renderer) return VisiblePositionRange(); // iterate over the lines @@ -1785,7 +2066,7 @@ VisiblePositionRange AccessibilityRenderObject::visiblePositionRangeForLine(unsi // last offset of the last line VisiblePosition visiblePos = m_renderer->document()->renderer()->positionForCoordinates(0, 0); VisiblePosition savedVisiblePos; - while (--lineCount != 0) { + while (--lineCount) { savedVisiblePos = visiblePos; visiblePos = nextLinePosition(visiblePos, 0); if (visiblePos.isNull() || visiblePos == savedVisiblePos) @@ -1900,9 +2181,8 @@ void AccessibilityRenderObject::setSelectedVisiblePositionRange(const VisiblePos return; // make selection and tell the document to use it. if it's zero length, then move to that position - if (range.start == range.end) { + if (range.start == range.end) m_renderer->document()->frame()->selection()->moveTo(range.start, true); - } else { VisibleSelection newSelection = VisibleSelection(range.start, range.end); m_renderer->document()->frame()->selection()->setSelection(newSelection); @@ -1998,7 +2278,7 @@ PlainTextRange AccessibilityRenderObject::doAXRangeForLine(unsigned lineNumber) // iterate to the specified line VisiblePosition visiblePos = visiblePositionForIndex(0); VisiblePosition savedVisiblePos; - for (unsigned lineCount = lineNumber; lineCount != 0; lineCount -= 1) { + for (unsigned lineCount = lineNumber; lineCount; lineCount -= 1) { savedVisiblePos = visiblePos; visiblePos = nextLinePosition(visiblePos, 0); if (visiblePos.isNull() || visiblePos == savedVisiblePos) @@ -2053,8 +2333,8 @@ String AccessibilityRenderObject::doAXStringForRange(const PlainTextRange& range if (isPasswordField()) return String(); - if (range.length == 0) - return ""; + if (!range.length) + return String(); if (!isTextControl()) return String(); @@ -2081,7 +2361,7 @@ AccessibilityObject* AccessibilityRenderObject::accessibilityImageMapHitTest(HTM if (!area) return 0; - HTMLMapElement *map = static_cast<HTMLMapElement*>(area->parent()); + HTMLMapElement* map = static_cast<HTMLMapElement*>(area->parent()); AccessibilityObject* parent = accessibilityParentForImageMap(map); if (!parent) return 0; @@ -2159,14 +2439,14 @@ bool AccessibilityRenderObject::shouldFocusActiveDescendant() const case ProgressIndicatorRole: case ToolbarRole: case OutlineRole: + case TreeRole: + case GridRole: /* FIXME: replace these with actual roles when they are added to AccessibilityRole composite alert alertdialog - grid status timer - tree */ return true; default: @@ -2176,19 +2456,22 @@ bool AccessibilityRenderObject::shouldFocusActiveDescendant() const AccessibilityObject* AccessibilityRenderObject::activeDescendant() const { - if (renderer()->node() && !renderer()->node()->isElementNode()) + if (!m_renderer) return 0; - Element* element = static_cast<Element*>(renderer()->node()); + + if (m_renderer->node() && !m_renderer->node()->isElementNode()) + return 0; + Element* element = static_cast<Element*>(m_renderer->node()); String activeDescendantAttrStr = element->getAttribute(aria_activedescendantAttr).string(); if (activeDescendantAttrStr.isNull() || activeDescendantAttrStr.isEmpty()) return 0; - Element* target = renderer()->document()->getElementById(activeDescendantAttrStr); + Element* target = document()->getElementById(activeDescendantAttrStr); if (!target) return 0; - AccessibilityObject* obj = renderer()->document()->axObjectCache()->getOrCreate(target->renderer()); + AccessibilityObject* obj = axObjectCache()->getOrCreate(target->renderer()); if (obj && obj->isAccessibilityRenderObject()) // an activedescendant is only useful if it has a renderer, because that's what's needed to post the notification return obj; @@ -2207,7 +2490,7 @@ void AccessibilityRenderObject::handleActiveDescendantChanged() AccessibilityRenderObject* activedescendant = static_cast<AccessibilityRenderObject*>(activeDescendant()); if (activedescendant && shouldFocusActiveDescendant()) - doc->axObjectCache()->postNotification(activedescendant->renderer(), AXObjectCache::AXFocusedUIElementChanged, true); + doc->axObjectCache()->postNotification(m_renderer, AXObjectCache::AXActiveDescendantChanged, true); } AccessibilityObject* AccessibilityRenderObject::correspondingControlForLabelElement() const @@ -2247,80 +2530,6 @@ AccessibilityObject* AccessibilityRenderObject::observableObject() const return 0; } - -typedef HashMap<String, AccessibilityRole, CaseFoldingHash> ARIARoleMap; - -struct RoleEntry { - String ariaRole; - AccessibilityRole webcoreRole; -}; - -static const ARIARoleMap& createARIARoleMap() -{ - const RoleEntry roles[] = { - { "application", LandmarkApplicationRole }, - { "article", DocumentArticleRole }, - { "banner", LandmarkBannerRole }, - { "button", ButtonRole }, - { "checkbox", CheckBoxRole }, - { "complementary", LandmarkComplementaryRole }, - { "contentinfo", LandmarkContentInfoRole }, - { "grid", TableRole }, - { "gridcell", CellRole }, - { "columnheader", ColumnHeaderRole }, - { "definition", DefinitionListDefinitionRole }, - { "document", DocumentRole }, - { "rowheader", RowHeaderRole }, - { "group", GroupRole }, - { "heading", HeadingRole }, - { "img", ImageRole }, - { "link", WebCoreLinkRole }, - { "list", ListRole }, - { "listitem", GroupRole }, - { "listbox", ListBoxRole }, - { "log", ApplicationLogRole }, - // "option" isn't here because it may map to different roles depending on the parent element's role - { "main", LandmarkMainRole }, - { "marquee", ApplicationMarqueeRole }, - { "menu", MenuRole }, - { "menubar", GroupRole }, - // "menuitem" isn't here because it may map to different roles depending on the parent element's role - { "menuitemcheckbox", MenuItemRole }, - { "menuitemradio", MenuItemRole }, - { "note", DocumentNoteRole }, - { "navigation", LandmarkNavigationRole }, - { "option", ListBoxOptionRole }, - { "presentation", IgnoredRole }, - { "progressbar", ProgressIndicatorRole }, - { "radio", RadioButtonRole }, - { "radiogroup", RadioGroupRole }, - { "region", DocumentRegionRole }, - { "row", RowRole }, - { "range", SliderRole }, - { "search", LandmarkSearchRole }, - { "separator", SplitterRole }, - { "slider", SliderRole }, - { "spinbutton", ProgressIndicatorRole }, - { "status", ApplicationStatusRole }, - { "textbox", TextAreaRole }, - { "timer", ApplicationTimerRole }, - { "toolbar", ToolbarRole }, - { "tooltip", UserInterfaceTooltipRole } - }; - ARIARoleMap& roleMap = *new ARIARoleMap; - - const unsigned numRoles = sizeof(roles) / sizeof(roles[0]); - for (unsigned i = 0; i < numRoles; ++i) - roleMap.set(roles[i].ariaRole, roles[i].webcoreRole); - return roleMap; -} - -static AccessibilityRole ariaRoleToWebCoreRole(String value) -{ - ASSERT(!value.isEmpty() && !value.isNull()); - static const ARIARoleMap& roleMap = createARIARoleMap(); - return roleMap.get(value); -} AccessibilityRole AccessibilityRenderObject::determineAriaRoleAttribute() const { @@ -2332,14 +2541,14 @@ AccessibilityRole AccessibilityRenderObject::determineAriaRoleAttribute() const if (role) return role; // selects and listboxes both have options as child roles, but they map to different roles within WebCore - if (equalIgnoringCase(ariaRole,"option")) { + if (equalIgnoringCase(ariaRole, "option")) { if (parentObjectUnignored()->ariaRoleAttribute() == MenuRole) return MenuItemRole; if (parentObjectUnignored()->ariaRoleAttribute() == ListBoxRole) return ListBoxOptionRole; } // an aria "menuitem" may map to MenuButton or MenuItem depending on its parent - if (equalIgnoringCase(ariaRole,"menuitem")) { + if (equalIgnoringCase(ariaRole, "menuitem")) { if (parentObjectUnignored()->ariaRoleAttribute() == GroupRole) return MenuButtonRole; if (parentObjectUnignored()->ariaRoleAttribute() == MenuRole) @@ -2418,7 +2627,7 @@ AccessibilityRole AccessibilityRenderObject::determineAccessibilityRole() if (m_renderer->isMenuList()) return PopUpButtonRole; - if (headingLevel() != 0) + if (headingLevel()) return HeadingRole; if (node && node->hasTagName(ddTag)) @@ -2436,12 +2645,24 @@ AccessibilityRole AccessibilityRenderObject::determineAccessibilityRole() return UnknownRole; } +AccessibilityOrientation AccessibilityRenderObject::orientation() const +{ + const AtomicString& ariaOrientation = getAttribute(aria_orientationAttr).string(); + if (equalIgnoringCase(ariaOrientation, "horizontal")) + return AccessibilityOrientationHorizontal; + if (equalIgnoringCase(ariaOrientation, "vertical")) + return AccessibilityOrientationVertical; + + return AccessibilityObject::orientation(); +} + bool AccessibilityRenderObject::isPresentationalChildOfAriaRole() const { // Walk the parent chain looking for a parent that has presentational children AccessibilityObject* parent; for (parent = parentObject(); parent && !parent->ariaRoleHasPresentationalChildren(); parent = parent->parentObject()) - ; + { } + return parent; } @@ -2474,20 +2695,27 @@ bool AccessibilityRenderObject::canSetFocusAttribute() const return false; switch (roleValue()) { - case WebCoreLinkRole: - case ImageMapLinkRole: - case TextFieldRole: - case TextAreaRole: - case ButtonRole: - case PopUpButtonRole: - case CheckBoxRole: - case RadioButtonRole: - case SliderRole: - return true; - default: - return false; + case WebCoreLinkRole: + case ImageMapLinkRole: + case TextFieldRole: + case TextAreaRole: + case ButtonRole: + case PopUpButtonRole: + case CheckBoxRole: + case RadioButtonRole: + case SliderRole: + return true; + default: + return false; } } + +bool AccessibilityRenderObject::canSetExpandedAttribute() const +{ + // An object can be expanded if it aria-expanded is true or false. + String ariaExpanded = getAttribute(aria_expandedAttr).string(); + return equalIgnoringCase(ariaExpanded, "true") || equalIgnoringCase(ariaExpanded, "false"); +} bool AccessibilityRenderObject::canSetValueAttribute() const { @@ -2531,16 +2759,18 @@ bool AccessibilityRenderObject::canHaveChildren() const // Elements that should not have children switch (roleValue()) { - case ImageRole: - case ButtonRole: - case PopUpButtonRole: - case CheckBoxRole: - case RadioButtonRole: - case StaticTextRole: - case ListBoxOptionRole: - return false; - default: - return true; + case ImageRole: + case ButtonRole: + case PopUpButtonRole: + case CheckBoxRole: + case RadioButtonRole: + case TabRole: + case StaticTextRole: + case ListBoxOptionRole: + case ScrollBarRole: + return false; + default: + return true; } } @@ -2604,17 +2834,42 @@ void AccessibilityRenderObject::addChildren() } } +void AccessibilityRenderObject::ariaTreeSelectedRows(AccessibilityChildrenVector& result) +{ + // Get all the rows. + AccessibilityChildrenVector allRows; + ariaTreeRows(allRows); + + // Determine which rows are selected. + bool isMultiselectable = elementAttributeValue(aria_multiselectableAttr); + + // Prefer active descendant over aria-selected. + AccessibilityObject* activeDesc = activeDescendant(); + if (activeDesc && activeDesc->isTreeItem()) { + result.append(activeDesc); + if (!isMultiselectable) + return; + } + + unsigned count = allRows.size(); + for (unsigned k = 0; k < count; ++k) { + if (allRows[k]->isSelected()) { + result.append(allRows[k]); + if (!isMultiselectable) + break; + } + } +} + void AccessibilityRenderObject::ariaListboxSelectedChildren(AccessibilityChildrenVector& result) { AccessibilityObject* child = firstChild(); - bool isMultiselectable = false; Element* element = static_cast<Element*>(renderer()->node()); if (!element || !element->isElementNode()) // do this check to ensure safety of static_cast above return; - String multiselectablePropertyStr = element->getAttribute("aria-multiselectable").string(); - isMultiselectable = equalIgnoringCase(multiselectablePropertyStr, "true"); + bool isMultiselectable = elementAttributeValue(aria_multiselectableAttr); while (child) { // every child should have aria-role option, and if so, check for selected attribute/state @@ -2625,7 +2880,7 @@ void AccessibilityRenderObject::ariaListboxSelectedChildren(AccessibilityChildre if (childRenderer && ariaRole == ListBoxOptionRole) { Element* childElement = static_cast<Element*>(childRenderer->node()); if (childElement && childElement->isElementNode()) { // do this check to ensure safety of static_cast above - String selectedAttrString = childElement->getAttribute("aria-selected").string(); + String selectedAttrString = childElement->getAttribute(aria_selectedAttr).string(); if (equalIgnoringCase(selectedAttrString, "true")) { result.append(child); if (isMultiselectable) @@ -2642,11 +2897,11 @@ void AccessibilityRenderObject::selectedChildren(AccessibilityChildrenVector& re ASSERT(result.isEmpty()); // only listboxes should be asked for their selected children. - if (ariaRoleAttribute() != ListBoxRole) { // native list boxes would be AccessibilityListBoxes, so only check for aria list boxes - ASSERT_NOT_REACHED(); - return; - } - return ariaListboxSelectedChildren(result); + AccessibilityRole role = roleValue(); + if (role == ListBoxRole) // native list boxes would be AccessibilityListBoxes, so only check for aria list boxes + ariaListboxSelectedChildren(result); + else if (role == TreeRole) + ariaTreeSelectedRows(result); } void AccessibilityRenderObject::ariaListboxVisibleChildren(AccessibilityChildrenVector& result) @@ -2673,6 +2928,17 @@ void AccessibilityRenderObject::visibleChildren(AccessibilityChildrenVector& res return ariaListboxVisibleChildren(result); } +void AccessibilityRenderObject::tabChildren(AccessibilityChildrenVector& result) +{ + ASSERT(roleValue() == TabListRole); + + unsigned length = m_children.size(); + for (unsigned i = 0; i < length; ++i) { + if (m_children[i]->isTabItem()) + result.append(m_children[i]); + } +} + const String& AccessibilityRenderObject::actionVerb() const { // FIXME: Need to add verbs for select elements. @@ -2685,20 +2951,20 @@ const String& AccessibilityRenderObject::actionVerb() const DEFINE_STATIC_LOCAL(const String, noAction, ()); switch (roleValue()) { - case ButtonRole: - return buttonAction; - case TextFieldRole: - case TextAreaRole: - return textFieldAction; - case RadioButtonRole: - return radioButtonAction; - case CheckBoxRole: - return isChecked() ? checkedCheckBoxAction : uncheckedCheckBoxAction; - case LinkRole: - case WebCoreLinkRole: - return linkAction; - default: - return noAction; + case ButtonRole: + return buttonAction; + case TextFieldRole: + case TextAreaRole: + return textFieldAction; + case RadioButtonRole: + return radioButtonAction; + case CheckBoxRole: + return isChecked() ? checkedCheckBoxAction : uncheckedCheckBoxAction; + case LinkRole: + case WebCoreLinkRole: + return linkAction; + default: + return noAction; } } @@ -2711,4 +2977,101 @@ void AccessibilityRenderObject::updateBackingStore() m_renderer->document()->updateLayoutIgnorePendingStylesheets(); } +static bool isLinkable(const AccessibilityRenderObject& object) +{ + if (!object.renderer()) + return false; + + // See https://wiki.mozilla.org/Accessibility/AT-Windows-API for the elements + // Mozilla considers linkable. + return object.isLink() || object.isImage() || object.renderer()->isText(); +} + +String AccessibilityRenderObject::stringValueForMSAA() const +{ + if (isLinkable(*this)) { + Element* anchor = anchorElement(); + if (anchor && anchor->hasTagName(aTag)) + return static_cast<HTMLAnchorElement*>(anchor)->href(); + } + + return stringValue(); +} + +bool AccessibilityRenderObject::isLinked() const +{ + if (!isLinkable(*this)) + return false; + + Element* anchor = anchorElement(); + if (!anchor || !anchor->hasTagName(aTag)) + return false; + + return !static_cast<HTMLAnchorElement*>(anchor)->href().isEmpty(); +} + +String AccessibilityRenderObject::nameForMSAA() const +{ + if (m_renderer && m_renderer->isText()) + return textUnderElement(); + + return title(); +} + +static bool shouldReturnTagNameAsRoleForMSAA(const Element& element) +{ + // See "document structure", + // https://wiki.mozilla.org/Accessibility/AT-Windows-API + // FIXME: Add the other tag names that should be returned as the role. + return element.hasTagName(h1Tag) || element.hasTagName(h2Tag) + || element.hasTagName(h3Tag) || element.hasTagName(h4Tag) + || element.hasTagName(h5Tag) || element.hasTagName(h6Tag); +} + +String AccessibilityRenderObject::stringRoleForMSAA() const +{ + if (!m_renderer) + return String(); + + Node* node = m_renderer->node(); + if (!node || !node->isElementNode()) + return String(); + + Element* element = static_cast<Element*>(node); + if (!shouldReturnTagNameAsRoleForMSAA(*element)) + return String(); + + return element->tagName(); +} + +String AccessibilityRenderObject::positionalDescriptionForMSAA() const +{ + // See "positional descriptions", + // https://wiki.mozilla.org/Accessibility/AT-Windows-API + if (isHeading()) + return "L" + String::number(headingLevel()); + + // FIXME: Add positional descriptions for other elements. + return String(); +} + +String AccessibilityRenderObject::descriptionForMSAA() const +{ + String description = positionalDescriptionForMSAA(); + if (!description.isEmpty()) + return description; + + description = accessibilityDescription(); + if (!description.isEmpty()) { + // From the Mozilla MSAA implementation: + // "Signal to screen readers that this description is speakable and is not + // a formatted positional information description. Don't localize the + // 'Description: ' part of this string, it will be parsed out by assistive + // technologies." + return "Description: " + description; + } + + return String(); +} + } // namespace WebCore |