Commit 60f0160e authored by cfleizach@apple.com's avatar cfleizach@apple.com
Browse files

Support ARIA "tab" roles

https://bugs.webkit.org/show_bug.cgi?id=30842

Reviewed by Beth Dakin.

WebCore: 

Implement support for ARIA "tab", "tabpanel" and "tablist".
As a consequence, we also needed to implement aria-selected
and aria-controls.

Tests: accessibility/aria-controls-with-tabs.html
       accessibility/aria-tab-roles.html

* accessibility/AXObjectCache.cpp:
* accessibility/AccessibilityObject.h:
* accessibility/AccessibilityRenderObject.cpp:
* accessibility/AccessibilityRenderObject.h:
* accessibility/mac/AccessibilityObjectWrapper.mm:
* html/HTMLAttributeNames.in:

WebKit: 

Add a localizable string for tab panel.

* English.lproj/Localizable.strings:
* StringsNotToBeLocalized.txt:

WebKit/mac: 

* WebCoreSupport/WebViewFactory.mm:
(-[WebViewFactory AXARIAContentGroupText:]):

WebKitTools: 

* DumpRenderTree/AccessibilityUIElement.cpp:
* DumpRenderTree/AccessibilityUIElement.h:
* DumpRenderTree/gtk/AccessibilityUIElementGtk.cpp:
* DumpRenderTree/mac/AccessibilityUIElementMac.mm:
* DumpRenderTree/win/AccessibilityUIElementWin.cpp:

LayoutTests: 

* accessibility/aria-controls-with-tabs-expected.txt: Added.
* accessibility/aria-controls-with-tabs.html: Added.
* accessibility/aria-tab-roles.html: Added.
* platform/gtk/Skipped:
* platform/mac/accessibility/aria-tab-roles-expected.txt: Added.
* platform/win/Skipped:



git-svn-id: http://svn.webkit.org/repository/webkit/trunk@50409 268f45cc-cd09-0410-ab3c-d52691b4dbfc
parent df622ebc
2009-11-02 Chris Fleizach <cfleizach@apple.com>
Reviewed by Beth Dakin.
Support ARIA "tab" roles
https://bugs.webkit.org/show_bug.cgi?id=30842
* accessibility/aria-controls-with-tabs-expected.txt: Added.
* accessibility/aria-controls-with-tabs.html: Added.
* accessibility/aria-tab-roles.html: Added.
* platform/gtk/Skipped:
* platform/mac/accessibility/aria-tab-roles-expected.txt: Added.
* platform/win/Skipped:
2009-11-02 Roland Steiner <rolandsteiner@chromium.org>
 
Reviewed by Dave Hyatt.
Crust
Veges
Test
Select Crust
Select Crust
This tests that the aria tab item becomes selected if either aria-selected is used, or if aria-controls points to an item that contains KB focus.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
PASS tab2.isSelected is true
PASS tab1.isSelected is false
PASS tab2.isSelected is false
PASS tab1.isSelected is true
PASS successfullyParsed is true
TEST COMPLETE
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<link rel="stylesheet" href="../fast/js/resources/js-test-style.css">
<script>
var successfullyParsed = false;
</script>
<script src="../fast/js/resources/js-test-pre.js"></script>
</head>
<body id="body">
<ul id="tablist_1" role="tablist">
<li id="tab_1" role="tab" tabindex="-1" class="">Crust</li>
<li id="tab_2" role="tab" tabindex="-1" aria-controls="panel_2" class="">Veges</li>
</ul>
<h3 tabindex=0 id="elementOutsideTabs">Test</h3>
<div id="panel_1" role="tabpanel" >
<h3 tabindex=0>Select Crust</h3>
</div>
<div id="panel_2" role="tabpanel" >
<h2 id="itemInPanel2" tabindex=0>Select Crust</h2>
</div>
<p id="description"></p>
<div id="console"></div>
<script>
description("This tests that the aria tab item becomes selected if either aria-selected is used, or if aria-controls points to an item that contains KB focus.");
if (window.accessibilityController) {
var body = accessibilityController.rootElement;
var tabList = body.childAtIndex(0).childAtIndex(0);
var tab1 = tabList.childAtIndex(0);
var tab2 = tabList.childAtIndex(1);
// we set KB focus to something in panel_2, which means that tab2 should become selected
// because it aria-controls panel_2
var panel2Item = document.getElementById("itemInPanel2");
panel2Item.focus();
shouldBe("tab2.isSelected", "true");
// reset KB focus and verify that neither tab is selected
document.getElementById("elementOutsideTabs").focus();
shouldBe("tab1.isSelected", "false");
shouldBe("tab2.isSelected", "false");
// Now we set aria-selected to be true on tab1 so that it should become selected
document.getElementById("tab_1").setAttribute("aria-selected", "true");
shouldBe("tab1.isSelected", "true");
}
successfullyParsed = true;
</script>
<script src="../fast/js/resources/js-test-post.js"></script>
</body>
</html>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<link rel="stylesheet" href="../fast/js/resources/js-test-style.css">
<script>
var successfullyParsed = false;
</script>
<script src="../fast/js/resources/js-test-pre.js"></script>
</head>
<body id="body">
<ul id="tablist_1" role="tablist">
<li id="tab_1" role="tab" tabindex="0" class="">Crust</li>
<li id="tab_2" role="tab" tabindex="0" class="">Veges</li>
</ul>
<div id="panel_1" role="tabpanel" aria-labelledby="tab_1" >
<h3>Select Crust</h3>
</div>
<p id="description"></p>
<div id="console"></div>
<script>
description("This tests that the aria roles for tab, tabpanel and tablist work as expected correctly.");
if (window.accessibilityController) {
var body = document.getElementById("body");
body.focus();
var tabList = accessibilityController.focusedElement.childAtIndex(0);
var tab1 = tabList.childAtIndex(0);
var tab2 = tabList.childAtIndex(1);
var tabPanel = accessibilityController.focusedElement.childAtIndex(1);
shouldBe("tabList.role", "'AXRole: AXTabGroup'");
shouldBe("tab1.role", "'AXRole: AXRadioButton'");
shouldBe("tab1.title", "'AXTitle: Crust'");
shouldBe("tab1.childrenCount", "0");
shouldBe("tab2.role", "'AXRole: AXRadioButton'");
shouldBe("tab2.title", "'AXTitle: Veges'");
shouldBe("tabPanel.role", "'AXRole: AXGroup'");
shouldBe("tabPanel.subrole", "'AXSubrole: AXTabPanel'");
}
successfullyParsed = true;
</script>
<script src="../fast/js/resources/js-test-post.js"></script>
</body>
</html>
......@@ -49,6 +49,7 @@ accessibility/aria-label.html
accessibility/aria-labelledby-stay-within.html
accessibility/aria-link-supports-press.html
accessibility/aria-readonly.html
accessibility/aria-tab-roles.html
accessibility/button-press-action.html
accessibility/canvas.html
accessibility/editable-webarea-context-menu-point.html
......
Crust
Veges
Select Crust
This tests that the aria roles for tab, tabpanel and tablist work as expected correctly.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
PASS tabList.role is 'AXRole: AXTabGroup'
PASS tab1.role is 'AXRole: AXRadioButton'
PASS tab1.title is 'AXTitle: Crust'
PASS tab1.childrenCount is 0
PASS tab2.role is 'AXRole: AXRadioButton'
PASS tab2.title is 'AXTitle: Veges'
PASS tabPanel.role is 'AXRole: AXGroup'
PASS tabPanel.subrole is 'AXSubrole: AXTabPanel'
PASS successfullyParsed is true
TEST COMPLETE
......@@ -363,6 +363,7 @@ accessibility/aria-presentational-role.html
accessibility/aria-readonly.html
accessibility/aria-roles.html
accessibility/aria-tables.html
accessibility/aria-tab-roles.html
accessibility/button-press-action.html
accessibility/canvas.html
accessibility/editable-webarea-context-menu-point.html
......
2009-11-02 Chris Fleizach <cfleizach@apple.com>
Reviewed by Beth Dakin.
Support ARIA "tab" roles
https://bugs.webkit.org/show_bug.cgi?id=30842
Implement support for ARIA "tab", "tabpanel" and "tablist".
As a consequence, we also needed to implement aria-selected
and aria-controls.
Tests: accessibility/aria-controls-with-tabs.html
accessibility/aria-tab-roles.html
* accessibility/AXObjectCache.cpp:
* accessibility/AccessibilityObject.h:
* accessibility/AccessibilityRenderObject.cpp:
* accessibility/AccessibilityRenderObject.h:
* accessibility/mac/AccessibilityObjectWrapper.mm:
* html/HTMLAttributeNames.in:
2009-10-27 Stephen White <senorblanco@chromium.org>
 
Reviewed by Dmitry Titov.
......@@ -141,7 +141,10 @@ AccessibilityObject* AXObjectCache::getOrCreate(RenderObject* renderer)
RefPtr<AccessibilityObject> newObj = 0;
if (renderer->isListBox())
newObj = AccessibilityListBox::create(renderer);
else if (node && (nodeIsAriaType(node, "list") || node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(dlTag)))
// If the node is aria role="list" or the aria role is empty and its a ul/ol/dl type (it shouldn't be a list if aria says otherwise).
else if (node && (nodeIsAriaType(node, "list")
|| (nodeIsAriaType(node, nullAtom) && (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(dlTag)))))
newObj = AccessibilityList::create(renderer);
// aria tables
......
......@@ -161,6 +161,9 @@ enum AccessibilityRole {
AnnotationRole,
SliderThumbRole,
IgnoredRole,
TabRole,
TabListRole,
TabPanelRole,
// ARIA Grouping roles
LandmarkApplicationRole,
......@@ -269,6 +272,8 @@ public:
virtual bool isTableCell() const { return false; };
virtual bool isFieldset() const { return false; };
virtual bool isGroup() const { return false; };
bool isTabList() const { return roleValue() == TabListRole; }
bool isTabItem() const { return roleValue() == TabRole; }
bool isRadioGroup() const { return roleValue() == RadioGroupRole; }
virtual bool isChecked() const { return false; };
......@@ -304,6 +309,7 @@ public:
virtual float maxValueForRange() const { return 0.0f; }
virtual float minValueForRange() const { return 0.0f; }
virtual AccessibilityObject* selectedRadioButton() { return 0; }
virtual AccessibilityObject* selectedTabItem() { return 0; }
virtual int layoutCount() const { return 0; }
static bool isARIAControl(AccessibilityRole);
static bool isARIAInput(AccessibilityRole);
......@@ -332,7 +338,6 @@ public:
void setRoleValue(AccessibilityRole role) { m_role = role; }
virtual AccessibilityRole roleValue() const { return m_role; }
virtual String ariaAccessibilityName(const String&) const { return String(); }
virtual String ariaLabeledByAttribute() const { return String(); }
virtual String ariaDescribedByAttribute() const { return String(); }
virtual String accessibilityDescription() const { return String(); }
......@@ -393,6 +398,7 @@ public:
virtual bool hasChildren() const { return m_haveChildren; }
virtual void selectedChildren(AccessibilityChildrenVector&) { }
virtual void visibleChildren(AccessibilityChildrenVector&) { }
virtual void tabChildren(AccessibilityChildrenVector&) { }
virtual bool shouldFocusActiveDescendant() const { return false; }
virtual AccessibilityObject* activeDescendant() const { return 0; }
virtual void handleActiveDescendantChanged() { }
......
......@@ -505,6 +505,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
{
......@@ -864,56 +882,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 +1019,7 @@ String AccessibilityRenderObject::title() const
|| ariaRole == MenuItemRole
|| ariaRole == MenuButtonRole
|| ariaRole == RadioButtonRole
|| ariaRole == TabRole
|| isHeading())
return textUnderElement();
......@@ -1001,11 +1031,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
......@@ -1562,9 +1591,55 @@ bool AccessibilityRenderObject::isSelected() const
if (!node)
return false;
if (equalIgnoringCase(getAttribute(aria_selectedAttr).string(), "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)
......@@ -2302,6 +2377,9 @@ static const ARIARoleMap& createARIARoleMap()
{ "slider", SliderRole },
{ "spinbutton", ProgressIndicatorRole },
{ "status", ApplicationStatusRole },
{ "tab", TabRole },
{ "tablist", TabListRole },
{ "tabpanel", TabPanelRole },
{ "textbox", TextAreaRole },
{ "timer", ApplicationTimerRole },
{ "toolbar", ToolbarRole },
......@@ -2536,6 +2614,7 @@ bool AccessibilityRenderObject::canHaveChildren() const
case PopUpButtonRole:
case CheckBoxRole:
case RadioButtonRole:
case TabRole:
case StaticTextRole:
case ListBoxOptionRole:
return false;
......@@ -2625,7 +2704,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)
......@@ -2673,6 +2752,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.
......
......@@ -120,6 +120,7 @@ public:
virtual float maxValueForRange() const;
virtual float minValueForRange() const;
virtual AccessibilityObject* selectedRadioButton();
virtual AccessibilityObject* selectedTabItem();
virtual int layoutCount() const;
virtual AccessibilityObject* doAccessibilityHitTest(const IntPoint&) const;
......@@ -168,7 +169,6 @@ public:
virtual PlainTextRange selectedTextRange() const;
virtual VisibleSelection selection() const;
virtual String stringValue() const;
virtual String ariaAccessibilityName(const String&) const;
virtual String ariaLabeledByAttribute() const;
virtual String title() const;
virtual String ariaDescribedByAttribute() const;
......@@ -202,6 +202,7 @@ public:
virtual bool canHaveChildren() const;
virtual void selectedChildren(AccessibilityChildrenVector&);
virtual void visibleChildren(AccessibilityChildrenVector&);
virtual void tabChildren(AccessibilityChildrenVector&);
virtual bool shouldFocusActiveDescendant() const;
virtual AccessibilityObject* activeDescendant() const;
virtual void handleActiveDescendantChanged();
......@@ -237,6 +238,7 @@ protected:
mutable bool m_childrenDirty;
void setRenderObject(RenderObject* renderer) { m_renderer = renderer; }
void ariaLabeledByElements(Vector<Element*>& elements) const;
virtual bool isDetached() const { return !m_renderer; }
......@@ -251,12 +253,16 @@ private:
AccessibilityRole determineAccessibilityRole();
AccessibilityRole determineAriaRoleAttribute() const;
bool isTabItemSelected() const;
IntRect checkboxOrRadioRect() const;
void addRadioButtonGroupMembers(AccessibilityChildrenVector& linkedUIElements) const;
AccessibilityObject* internalLinkElement() const;
AccessibilityObject* accessibilityImageMapHitTest(HTMLAreaElement*, const IntPoint&) const;
AccessibilityObject* accessibilityParentForImageMap(HTMLMapElement* map) const;
String accessibilityDescriptionForElements(Vector<Element*> &elements) const;
void elementsFromAttribute(Vector<Element*>& elements, const QualifiedName& name) const;
void markChildrenDirty() const { m_childrenDirty = true; }
};
......
......@@ -600,6 +600,7 @@ static WebCoreTextMarkerRange* textMarkerRangeFromVisiblePositions(VisiblePositi
static NSArray* groupAttrs = nil;
static NSArray* inputImageAttrs = nil;
static NSArray* passwordFieldAttrs = nil;
static NSArray *tabListAttrs = nil;
NSMutableArray* tempArray;
if (attributes == nil) {
attributes = [[NSArray alloc] initWithObjects: NSAccessibilityRoleAttribute,
......@@ -794,6 +795,13 @@ static WebCoreTextMarkerRange* textMarkerRangeFromVisiblePositions(VisiblePositi
passwordFieldAttrs = [[NSArray alloc] initWithArray:tempArray];
[tempArray release];
}
if (tabListAttrs == nil) {