AX: Focusable elements without a role should not be ignored
authordmazzoni@google.com <dmazzoni@google.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 29 Aug 2012 07:56:45 +0000 (07:56 +0000)
committerdmazzoni@google.com <dmazzoni@google.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 29 Aug 2012 07:56:45 +0000 (07:56 +0000)
https://bugs.webkit.org/show_bug.cgi?id=94302

Reviewed by Chris Fleizach.

Source/WebCore:

Changes the accessibility logic so that a generic element that's focusable is
not ignored for accessibility, and returns its inner text as its title. That way
if you Tab to the element, a reasonable accessibility notification is generated.

One exception is the body element, because focusing the body is equivalent to
blurring the current focused element and does not result in a "focus" accessibility
notification.

Also fixes logic that determined if an element was contentEditable by making
sure it catches the case with no attribute value (e.g. <div contentEditable>),
which also implies contentEditable=true according to the spec.

Test: accessibility/focusable-div.html

* accessibility/AccessibilityRenderObject.cpp:
(WebCore):
(WebCore::nodeHasContentEditableAttributeSet):
(WebCore::AccessibilityRenderObject::title):
(WebCore::AccessibilityRenderObject::accessibilityIsIgnored):

LayoutTests:

Adds a new test to make sure that a generic focusable element (like a div with tabindex=0)
can get focus and return an appropriate title, just like a form control or element with
an ARIA role.

Modifies three existing tests that were previously assuming that a focusable node
with no role would be ignored for accessibility ("accessibilityIsIgnored").

* accessibility/editable-webarea-context-menu-point.html:
* accessibility/focusable-div-expected.txt: Added.
* accessibility/focusable-div.html: Added.
* accessibility/table-detection.html:
* platform/mac/accessibility/listbox-hit-test.html:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@126970 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
LayoutTests/ChangeLog
LayoutTests/accessibility/editable-webarea-context-menu-point.html
LayoutTests/accessibility/focusable-div-expected.txt [new file with mode: 0644]
LayoutTests/accessibility/focusable-div.html [new file with mode: 0644]
LayoutTests/accessibility/table-detection.html
LayoutTests/platform/mac/accessibility/listbox-hit-test.html
Source/WebCore/ChangeLog
Source/WebCore/accessibility/AccessibilityNodeObject.cpp
Source/WebCore/accessibility/AccessibilityNodeObject.h
Source/WebCore/accessibility/AccessibilityObject.cpp
Source/WebCore/accessibility/AccessibilityObject.h
Source/WebCore/accessibility/AccessibilityRenderObject.cpp
Source/WebCore/accessibility/AccessibilityRenderObject.h

index c476325..51c049d 100644 (file)
@@ -1,3 +1,23 @@
+2012-08-28  Dominic Mazzoni  <dmazzoni@google.com>
+
+        AX: Focusable elements without a role should not be ignored
+        https://bugs.webkit.org/show_bug.cgi?id=94302
+
+        Reviewed by Chris Fleizach.
+
+        Adds a new test to make sure that a generic focusable element (like a div with tabindex=0)
+        can get focus and return an appropriate title, just like a form control or element with
+        an ARIA role.
+
+        Modifies three existing tests that were previously assuming that a focusable node
+        with no role would be ignored for accessibility ("accessibilityIsIgnored").
+
+        * accessibility/editable-webarea-context-menu-point.html:
+        * accessibility/focusable-div-expected.txt: Added.
+        * accessibility/focusable-div.html: Added.
+        * accessibility/table-detection.html:
+        * platform/mac/accessibility/listbox-hit-test.html:
+
 2012-08-28  Joone Hur  <joone.hur@intel.com>
 
         [EFL] Unreviewed gardening on the 64bit build bot.
index 885f25d..7371edf 100644 (file)
@@ -19,6 +19,7 @@ This tests that the click point, used for opening context menus, changes based o
 
           var body = document.getElementById("body");
           body.focus();
+          var axWebArea = accessibilityController.focusedElement.parentElement();
 
           x = body.offsetLeft + 10;
           y = body.offsetTop + 10;
@@ -29,7 +30,7 @@ This tests that the click point, used for opening context menus, changes based o
           eventSender.mouseDown();
           eventSender.mouseUp();
 
-          var clickPointX1 = accessibilityController.focusedElement.clickPointX;
+          var clickPointX1 = axWebArea.clickPointX;
 
           x = body.offsetLeft + 100 + 10;
           y = body.offsetTop + 10;
@@ -40,7 +41,7 @@ This tests that the click point, used for opening context menus, changes based o
           eventSender.mouseDown();
           eventSender.mouseUp();
 
-          var clickPointX2 = accessibilityController.focusedElement.clickPointX;
+          var clickPointX2 = axWebArea.clickPointX;
           var succeeded = clickPointX2 != clickPointX1;
           shouldBe("succeeded", "true");
 
diff --git a/LayoutTests/accessibility/focusable-div-expected.txt b/LayoutTests/accessibility/focusable-div-expected.txt
new file mode 100644 (file)
index 0000000..d384426
--- /dev/null
@@ -0,0 +1,20 @@
+A
+B
+C
+This test makes sure that a generic focusable div can get accessibility focus.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS document.activeElement == link is true
+PASS lastChar(axLink.title) is "A"
+PASS document.activeElement == div is true
+PASS lastChar(axDiv.title) is "B"
+PASS document.activeElement == div2 is true
+PASS lastChar(axDiv2.title) is "C"
+PASS document.activeElement == div3 is true
+PASS lastChar(axDiv3.description) is "D"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/accessibility/focusable-div.html b/LayoutTests/accessibility/focusable-div.html
new file mode 100644 (file)
index 0000000..adfffbf
--- /dev/null
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<script src="../fast/js/resources/js-test-pre.js"></script>
+
+<a id="link" href="#">A</a>
+<div id="div" tabindex="0">B</div>
+<div id="div2" tabindex="0"><div></div>C</div>
+<div id="div3" tabindex="0" aria-label="D"></div>
+
+<div id="console"></div>
+<script>
+description("This test makes sure that a generic focusable div can get accessibility focus.");
+
+if (window.testRunner && window.accessibilityController) {
+    window.testRunner.dumpAsText();
+
+    function lastChar(str) {
+        return str.substr(str.length - 1);
+    }
+
+    var link = document.getElementById('link');
+    link.focus();
+    shouldBe("document.activeElement == link", "true");
+    window.axLink = accessibilityController.focusedElement;
+    shouldBe("lastChar(axLink.title)", "\"A\"");
+
+    var div = document.getElementById('div');
+    div.focus();
+    shouldBe("document.activeElement == div", "true");
+    window.axDiv = accessibilityController.focusedElement;
+    shouldBe("lastChar(axDiv.title)", "\"B\"");
+
+    var div2 = document.getElementById('div2');
+    div2.focus();
+    shouldBe("document.activeElement == div2", "true");
+    window.axDiv2 = accessibilityController.focusedElement;
+    shouldBe("lastChar(axDiv2.title)", "\"C\"");
+
+    var div3 = document.getElementById('div3');
+    div3.focus();
+    shouldBe("document.activeElement == div3", "true");
+    window.axDiv3 = accessibilityController.focusedElement;
+    shouldBe("lastChar(axDiv3.description)", "\"D\"");
+}
+
+</script>
+
+<script src="../fast/js/resources/js-test-post.js"></script>
+</body>
+</html>
index 6620473..1281228 100644 (file)
     <table class="nmTB" cellpadding="0" cellspacing="0"><tr><td class="nmIBD" id="nmb" name="nmb" nm_sn="3032552" nm_suf="" CM_sf="Ex" CM="NewsMenuL1" pn="newsmenu" ct="nm0" cn="Politics"><a class="nmLBD" href="/id/3032553">Politics</a></td></tr><tr><td class="nmIB" id="nmb" name="nmb" nm_sn="18970411" nm_suf="" CM_sf="Ex" CM="NewsMenuL1" pn="newsmenu" ct="nxf" cn="Decision '08"><a class="nmLB" href="/id/18970417">Decision '08</a></td></tr><tr><td class="nmIB" id="nmb" name="nmb" nm_sn="18296896" nm_suf="" CM_sf="Ex" CM="NewsMenuL1" pn="newsmenu" ct="nxf" cn="The debates"><a class="nmLB" href="/id/18296908">The debates</a></td></tr><tr><td class="nmIB" id="nmb" name="nmb" nm_sn="21491043" nm_suf="" CM_sf="Ex" CM="NewsMenuL1" pn="newsmenu" ct="nxf" cn="The White House"><a class="nmLB" href="/id/21672863">The White House</a></td></tr><tr><td class="nmIB" id="nmb" name="nmb" nm_sn="21491571" nm_suf="" CM_sf="Ex" CM="NewsMenuL1" pn="newsmenu" ct="nxf" cn="Capitol Hill"><a class="nmLB" href="/id/21672985">Capitol Hill</a></td></tr><tr><td class="nmIB" id="nmb" name="nmb" nm_sn="14016004" nm_suf="" CM_sf="Ex" CM="NewsMenuL1" pn="newsmenu" ct="nxf" cn="National Journal"><a class="nmLB" href="/id/14016001">National Journal</a></td></tr><tr><td class="nmIB" id="nmb" name="nmb" nm_sn="19748467" nm_suf="" CM_sf="Ex" CM="NewsMenuL1" pn="newsmenu" ct="nxf" cn="New York Times"><a class="nmLB" href="/id/19747577">New York Times</a></td></tr></table>
 
     // this should be a table because it's editable
-    <div contenteditable>
-      <table style='border: 1px solid black'>
-        <tr><td >asdf</td><td>asdf</td></tr>
-      </table>
-    </div>
+
+    <table style='border: 1px solid black' contenteditable>
+      <tr><td >asdf</td><td>asdf</td></tr>
+    </table>
 
     <div id="result"></div>
     
index a7a54b3..5b543d2 100644 (file)
@@ -5,7 +5,7 @@
 </head>
 <body id="body">
 
-<div tabindex=0 id="region">
+<div tabindex=0 id="region" title="region">
 <select size=20>
 <option>test option that spans the width of the cell. test option that spans the width of the cell</option>
 <option>test option that spans the width of the cell. test option that spans the width of the cell</option>
index 9f8c3f9..234085f 100644 (file)
@@ -1,3 +1,30 @@
+2012-08-28  Dominic Mazzoni  <dmazzoni@google.com>
+
+        AX: Focusable elements without a role should not be ignored
+        https://bugs.webkit.org/show_bug.cgi?id=94302
+
+        Reviewed by Chris Fleizach.
+
+        Changes the accessibility logic so that a generic element that's focusable is
+        not ignored for accessibility, and returns its inner text as its title. That way
+        if you Tab to the element, a reasonable accessibility notification is generated.
+
+        One exception is the body element, because focusing the body is equivalent to
+        blurring the current focused element and does not result in a "focus" accessibility
+        notification.
+
+        Also fixes logic that determined if an element was contentEditable by making
+        sure it catches the case with no attribute value (e.g. <div contentEditable>),
+        which also implies contentEditable=true according to the spec.
+
+        Test: accessibility/focusable-div.html
+
+        * accessibility/AccessibilityRenderObject.cpp:
+        (WebCore):
+        (WebCore::nodeHasContentEditableAttributeSet):
+        (WebCore::AccessibilityRenderObject::title):
+        (WebCore::AccessibilityRenderObject::accessibilityIsIgnored):
+
 2012-08-29  Adam Barth  <abarth@webkit.org>
 
         Deploy ASCIILiteral hotness throughout WebCore
index fa74107..ec09f4c 100644 (file)
@@ -384,4 +384,15 @@ AccessibilityRole AccessibilityNodeObject::remapAriaRoleDueToParent(Accessibilit
     return role;
 }   
 
+// If you call node->rendererIsEditable() since that will return true if an ancestor is editable.
+// This only returns true if this is the element that actually has the contentEditable attribute set.
+bool AccessibilityNodeObject::hasContentEditableAttributeSet() const
+{
+    if (!hasAttribute(contenteditableAttr))
+        return false;
+    const AtomicString& contentEditableValue = getAttribute(contenteditableAttr);
+    // Both "true" (case-insensitive) and the empty string count as true.
+    return contentEditableValue.isEmpty() || equalIgnoringCase(contentEditableValue, "true");
+}
+
 } // namespace WebCore
index e0a2cad..518db88 100644 (file)
@@ -98,6 +98,7 @@ protected:
     AccessibilityRole ariaRoleAttribute() const;
     AccessibilityRole determineAriaRoleAttribute() const;
     AccessibilityRole remapAriaRoleDueToParent(AccessibilityRole) const;
+    bool hasContentEditableAttributeSet() const;
 
 private:
     Node* m_node;
index f867ec3..5f12c03 100644 (file)
@@ -1294,6 +1294,19 @@ const AtomicString& AccessibilityObject::invalidStatus() const
     return ariaInvalid;
 }
  
+bool AccessibilityObject::hasAttribute(const QualifiedName& attribute) const
+{
+    Node* elementNode = node();
+    if (!elementNode)
+        return false;
+    
+    if (!elementNode->isElementNode())
+        return false;
+    
+    Element* element = static_cast<Element*>(elementNode);
+    return element->fastHasAttribute(attribute);
+}
+    
 const AtomicString& AccessibilityObject::getAttribute(const QualifiedName& attribute) const
 {
     Node* elementNode = node();
index e67165a..8666912 100644 (file)
@@ -585,6 +585,7 @@ public:
     bool isAncestorOfObject(const AccessibilityObject*) const;
     
     static AccessibilityRole ariaRoleToWebCoreRole(const String&);
+    bool hasAttribute(const QualifiedName&) const;
     const AtomicString& getAttribute(const QualifiedName&) const;
 
     virtual VisiblePositionRange visiblePositionRange() const { return VisiblePositionRange(); }
index 7fa5d3b..7336482 100644 (file)
@@ -1401,7 +1401,12 @@ String AccessibilityRenderObject::title() const
     
     if (isHeading() || isLink())
         return textUnderElement();
-    
+
+    // If it's focusable but it's not content editable or a known control type, then it will appear to
+    // the user as a single atomic object, so we should use its text as the default title.
+    if (isGenericFocusableElement())
+        return textUnderElement();
+
     return String();
 }
 
@@ -1938,12 +1943,8 @@ bool AccessibilityRenderObject::accessibilityIsIgnored() const
     // Anything that is content editable should not be ignored.
     // However, one cannot just call node->rendererIsEditable() since that will ask if its parents
     // are also editable. Only the top level content editable region should be exposed.
-    if (node && node->isElementNode()) {
-        Element* element = static_cast<Element*>(node);
-        const AtomicString& contentEditable = element->getAttribute(contenteditableAttr);
-        if (equalIgnoringCase(contentEditable, "true"))
-            return false;
-    }
+    if (hasContentEditableAttributeSet())
+        return false;
     
     // List items play an important role in defining the structure of lists. They should not be ignored.
     if (roleValue() == ListItemRole)
@@ -1953,7 +1954,7 @@ bool AccessibilityRenderObject::accessibilityIsIgnored() const
     if (supportsARIAAttributes())
         return false;
     
-    if (m_renderer->isBlockFlow() && m_renderer->childrenInline())
+    if (m_renderer->isBlockFlow() && m_renderer->childrenInline() && !canSetFocusAttribute())
         return !toRenderBlock(m_renderer)->firstLineBox() && !mouseButtonListener();
     
     // ignore images seemingly used as spacers
@@ -3118,6 +3119,31 @@ bool AccessibilityRenderObject::isDescendantOfElementType(const QualifiedName& t
     }
     return false;
 }
+
+bool AccessibilityRenderObject::isGenericFocusableElement() const
+{
+    if (!canSetFocusAttribute())
+        return false;
+
+    // If it's a control, it's not generic.
+    if (isControl())
+        return false;
+
+    // If the content editable attribute is set on this element, that's the reason
+    // it's focusable, and existing logic should handle this case already - so it's not a
+    // generic focusable element.
+    if (hasContentEditableAttributeSet())
+        return false;
+
+    // The web area and body element are both focusable, but existing logic handles these
+    // cases already, so we don't need to include them here.
+    if (roleValue() == WebAreaRole)
+        return false;
+    if (node() && node()->hasTagName(bodyTag))
+        return false;
+
+    return true;
+}
     
 AccessibilityRole AccessibilityRenderObject::determineAccessibilityRole()
 {
index 288a942..fcff4fa 100644 (file)
@@ -300,6 +300,8 @@ private:
     bool renderObjectIsObservable(RenderObject*) const;
     RenderObject* renderParentObject() const;
     bool isDescendantOfElementType(const QualifiedName& tagName) const;
+    // This returns true if it's focusable but it's not content editable and it's not a control or ARIA control.
+    bool isGenericFocusableElement() const;
 
     void addTextFieldChildren();
     void addImageMapChildren();