AX: Aria-activedescendant not supported
authorcfleizach@apple.com <cfleizach@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 18 Jan 2018 17:26:23 +0000 (17:26 +0000)
committercfleizach@apple.com <cfleizach@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 18 Jan 2018 17:26:23 +0000 (17:26 +0000)
https://bugs.webkit.org/show_bug.cgi?id=161734
<rdar://problem/28202679>

Reviewed by Joanmarie Diggs.

Source/WebCore:

When a combo-box owns/controls a list/listbox/grid/tree, the owned element needs to check the active-descendant of the combobox when
checking if it has selected children.
The target of the selection change notification should also be the owned element in these cases.

Test: accessibility/aria-combobox-controlling-list.html

* accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::selectedListItem):
* accessibility/AccessibilityObject.h:
* accessibility/AccessibilityRenderObject.cpp:
(WebCore::AccessibilityRenderObject::targetElementForActiveDescendant const):
(WebCore::AccessibilityRenderObject::handleActiveDescendantChanged):
(WebCore::AccessibilityRenderObject::canHaveSelectedChildren const):
(WebCore::AccessibilityRenderObject::selectedChildren):
* accessibility/AccessibilityRenderObject.h:
* accessibility/mac/AXObjectCacheMac.mm:
(WebCore::AXObjectCache::postPlatformNotification):

LayoutTests:

* accessibility/aria-combobox-control-owns-elements-expected.txt: Added.
* accessibility/aria-combobox-control-owns-elements.html: Added.

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

LayoutTests/ChangeLog
LayoutTests/accessibility/aria-combobox-control-owns-elements-expected.txt [new file with mode: 0644]
LayoutTests/accessibility/aria-combobox-control-owns-elements.html [new file with mode: 0644]
LayoutTests/accessibility/lists-expected.txt
Source/WebCore/ChangeLog
Source/WebCore/accessibility/AccessibilityObject.cpp
Source/WebCore/accessibility/AccessibilityObject.h
Source/WebCore/accessibility/AccessibilityRenderObject.cpp
Source/WebCore/accessibility/AccessibilityRenderObject.h
Source/WebCore/accessibility/mac/AXObjectCacheMac.mm

index b901e73..b56558f 100644 (file)
@@ -1,3 +1,14 @@
+2018-01-18  Chris Fleizach  <cfleizach@apple.com>
+
+        AX: Aria-activedescendant not supported
+        https://bugs.webkit.org/show_bug.cgi?id=161734
+        <rdar://problem/28202679>
+
+        Reviewed by Joanmarie Diggs.
+
+        * accessibility/aria-combobox-control-owns-elements-expected.txt: Added.
+        * accessibility/aria-combobox-control-owns-elements.html: Added.
+
 2018-01-18  Per Arne Vollan  <pvollan@apple.com>
 
         Mark fast/forms/auto-fill-button/input-strong-password-auto-fill-button.html as failing on Windows.
diff --git a/LayoutTests/accessibility/aria-combobox-control-owns-elements-expected.txt b/LayoutTests/accessibility/aria-combobox-control-owns-elements-expected.txt
new file mode 100644 (file)
index 0000000..536d331
--- /dev/null
@@ -0,0 +1,36 @@
+
+item1
+item2
+
+item1
+item2
+
+cell1
+
+treeitem1
+treeitem2
+This tests variations of the comboboxes and elements it can control and own. Then verifies the active-descendant is reflected correctly.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS list.selectedChildrenCount is 0
+PASS list.selectedChildrenCount is 1
+PASS list.selectedChildAtIndex(0).isEqual(listitem1) is true
+PASS listbox.selectedChildrenCount is 0
+PASS listbox.selectedChildrenCount is 1
+PASS listbox.selectedChildAtIndex(0).isEqual(option2_1) is true
+PASS grid.selectedChildrenCount is 0
+PASS grid.selectedChildrenCount is 1
+PASS grid.selectedChildAtIndex(0).isEqual(row3_1) is true
+PASS tree.selectedChildrenCount is 0
+PASS tree.selectedChildrenCount is 1
+PASS tree.selectedChildAtIndex(0).isEqual(treeitem4_1) is true
+Received AXSelectedChildrenChanged for List1
+Received AXSelectedChildrenChanged for Listbox2
+Received AXSelectedRowsChanged for Grid3
+Received AXSelectedRowsChanged for Tree4
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/accessibility/aria-combobox-control-owns-elements.html b/LayoutTests/accessibility/aria-combobox-control-owns-elements.html
new file mode 100644 (file)
index 0000000..5470ce6
--- /dev/null
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<script src="../resources/js-test-pre.js"></script>
+<script src="../resources/accessibility-helper.js"></script>
+</head>
+<body id="body">
+
+<!-- Example 1: controls a list -->
+<input type="text" role="combobox" id="combobox1" aria-controls="list1" aria-label="Combobox1">
+<div role="list" id="list1" aria-label="List1">
+<div role="listitem" id="item1_1">item1</div>
+<div role="listitem" id="item1_2">item2</div>
+</div>
+
+<!-- Example 2: owns a listbox -->
+<input type="text" role="combobox" id="combobox2" aria-owns="listbox2" aria-label="Combobox2">
+<div role="listbox" id="listbox2" aria-label="Listbox2">
+<div role="option" id="option2_1">item1</div>
+<div role="option" id="option2_2">item2</div>
+</div>
+
+<!-- Example 3: owns a grid -->
+<input type="text" role="combobox" id="combobox3" aria-owns="grid3" aria-label="Combobox3">
+<div role="grid" id="grid3" aria-label="Grid3">
+<div role="row" id="row3_1">
+<div role="gridcell" id="gridcell3_1">cell1</div>
+</div>
+</div>
+
+<!-- Example 3: owns a tree -->
+<input type="text" role="combobox" id="combobox4" aria-owns="tree4" aria-label="Combobox4">
+<div role="tree" id="tree4" aria-label="Tree4">
+<div role="treeitem" id="treeitem4_1">treeitem1</div>
+<div role="treeitem" id="treeitem4_2">treeitem2</div>
+</div>
+
+<p id="description"></p>
+<div id="console"></div>
+
+<script>
+
+    description("This tests variations of the comboboxes and elements it can control and own. Then verifies the active-descendant is reflected correctly.");
+
+    if (window.accessibilityController) {
+        window.jsTestIsAsync = true;
+        var selectedChildrenChangeCount = 0;
+
+        window.accessibilityController.addNotificationListener(function (target, notification) {
+            if (notification == "AXSelectedChildrenChanged" || notification == "AXSelectedRowsChanged") {
+                selectedChildrenChangeCount++;
+                var targetString = platformValueForW3CName(target);
+                debug("Received " + notification + " for " + targetString);
+                if (selectedChildrenChangeCount == 4) {
+                    accessibilityController.removeNotificationListener();
+                    finishJSTest(); 
+                }
+            }
+        });
+
+        // Example 1: aria-controls a list.
+        document.getElementById("combobox1").focus();
+        var list = accessibilityController.accessibleElementById("list1");
+        shouldBe("list.selectedChildrenCount", "0");
+        // Set active-descendant, verify notification is sent and that list has correct selected children.
+        document.getElementById("combobox1").setAttribute("aria-activedescendant", "item1_1");
+        var listitem1 = accessibilityController.accessibleElementById("item1_1");
+        shouldBe("list.selectedChildrenCount", "1");
+        shouldBeTrue("list.selectedChildAtIndex(0).isEqual(listitem1)");
+
+        // Example 2: aria-owns a listbox.
+        document.getElementById("combobox2").focus();
+        var listbox = accessibilityController.accessibleElementById("listbox2");
+        shouldBe("listbox.selectedChildrenCount", "0");
+        // Set active-descendant, verify notification is sent and that list has correct selected children.
+        document.getElementById("combobox2").setAttribute("aria-activedescendant", "option2_1");
+        var option2_1 = accessibilityController.accessibleElementById("option2_1");
+        shouldBe("listbox.selectedChildrenCount", "1");
+        shouldBeTrue("listbox.selectedChildAtIndex(0).isEqual(option2_1)");
+
+        // Example 3: aria-owns a grid.
+        document.getElementById("combobox3").focus();
+        var grid = accessibilityController.accessibleElementById("grid3");
+        shouldBe("grid.selectedChildrenCount", "0");
+        // Set active-descendant, verify notification is sent and that list has correct selected children.
+        document.getElementById("combobox3").setAttribute("aria-activedescendant", "row3_1");
+        var row3_1 = accessibilityController.accessibleElementById("row3_1");
+        shouldBe("grid.selectedChildrenCount", "1");
+        shouldBeTrue("grid.selectedChildAtIndex(0).isEqual(row3_1)");
+
+        // Example 4: aria-owns a tree.
+        document.getElementById("combobox4").focus();
+        var tree = accessibilityController.accessibleElementById("tree4");
+        shouldBe("tree.selectedChildrenCount", "0");
+        // Set active-descendant, verify notification is sent and that list has correct selected children.
+        document.getElementById("combobox4").setAttribute("aria-activedescendant", "treeitem4_1");
+        var treeitem4_1 = accessibilityController.accessibleElementById("treeitem4_1");
+        shouldBe("tree.selectedChildrenCount", "1");
+        shouldBeTrue("tree.selectedChildAtIndex(0).isEqual(treeitem4_1)");
+    }
+
+</script>
+
+<script src="../resources/js-test-post.js"></script>
+</body>
+</html>
+
index dffe07d..08f00a5 100644 (file)
@@ -38,7 +38,7 @@ AXDOMClassList: <array of size 0>
 AXFocusableAncestor: <AXList>
 AXEditableAncestor: (null)
 AXHighestEditableAncestor: (null)
-AXSelectedChildren: (null)
+AXSelectedChildren: <array of size 0>
 AXVisibleChildren: <array of size 2>
 AXOrientation: AXVerticalOrientation
 AXTitleUIElement: (null)
@@ -72,7 +72,7 @@ AXDOMClassList: <array of size 0>
 AXFocusableAncestor: <AXList>
 AXEditableAncestor: (null)
 AXHighestEditableAncestor: (null)
-AXSelectedChildren: (null)
+AXSelectedChildren: <array of size 0>
 AXVisibleChildren: <array of size 2>
 AXOrientation: AXVerticalOrientation
 AXTitleUIElement: (null)
index 346748d..20ed9a3 100644 (file)
@@ -1,3 +1,29 @@
+2018-01-18  Chris Fleizach  <cfleizach@apple.com>
+
+        AX: Aria-activedescendant not supported
+        https://bugs.webkit.org/show_bug.cgi?id=161734
+        <rdar://problem/28202679>
+
+        Reviewed by Joanmarie Diggs.
+
+        When a combo-box owns/controls a list/listbox/grid/tree, the owned element needs to check the active-descendant of the combobox when
+        checking if it has selected children. 
+        The target of the selection change notification should also be the owned element in these cases.
+
+        Test: accessibility/aria-combobox-controlling-list.html
+
+        * accessibility/AccessibilityObject.cpp:
+        (WebCore::AccessibilityObject::selectedListItem):
+        * accessibility/AccessibilityObject.h:
+        * accessibility/AccessibilityRenderObject.cpp:
+        (WebCore::AccessibilityRenderObject::targetElementForActiveDescendant const):
+        (WebCore::AccessibilityRenderObject::handleActiveDescendantChanged):
+        (WebCore::AccessibilityRenderObject::canHaveSelectedChildren const):
+        (WebCore::AccessibilityRenderObject::selectedChildren):
+        * accessibility/AccessibilityRenderObject.h:
+        * accessibility/mac/AXObjectCacheMac.mm:
+        (WebCore::AXObjectCache::postPlatformNotification):
+
 2018-01-17  Per Arne Vollan  <pvollan@apple.com>
 
         REGRESSION (r224780): Text stroke not applied to video captions.
index 572a2f0..c7ae014 100644 (file)
@@ -3410,6 +3410,16 @@ bool AccessibilityObject::isContainedByPasswordField() const
     Element* element = node->shadowHost();
     return is<HTMLInputElement>(element) && downcast<HTMLInputElement>(*element).isPasswordField();
 }
+    
+AccessibilityObject* AccessibilityObject::selectedListItem()
+{
+    for (const auto& child : children()) {
+        if (child->isListItem() && (child->isSelected() || child->isActiveDescendantOfFocusedContainer()))
+            return child.get();
+    }
+    
+    return nullptr;
+}
 
 void AccessibilityObject::ariaElementsFromAttribute(AccessibilityChildrenVector& children, const QualifiedName& attributeName) const
 {
index c60c840..6a2fff8 100644 (file)
@@ -660,6 +660,7 @@ public:
     virtual float stepValueForRange() const { return 0.0f; }
     virtual AccessibilityObject* selectedRadioButton() { return nullptr; }
     virtual AccessibilityObject* selectedTabItem() { return nullptr; }
+    AccessibilityObject* selectedListItem();
     virtual int layoutCount() const { return 0; }
     virtual double estimatedLoadingProgress() const { return 0; }
     static bool isARIAControl(AccessibilityRole);
index dfc4f79..ae3cec7 100644 (file)
@@ -2495,6 +2495,18 @@ void AccessibilityRenderObject::handleAriaExpandedChanged()
     else
         cache->postNotification(this, document(), AXObjectCache::AXExpandedChanged);
 }
+    
+RenderObject* AccessibilityRenderObject::targetElementForActiveDescendant(const QualifiedName& attributeName, AccessibilityObject* activeDescendant) const
+{
+    AccessibilityObject::AccessibilityChildrenVector elements;
+    ariaElementsFromAttribute(elements, attributeName);
+    for (auto element : elements) {
+        if (activeDescendant->isDescendantOfObject(element.get()))
+            return element->renderer();
+    }
+
+    return nullptr;
+}
 
 void AccessibilityRenderObject::handleActiveDescendantChanged()
 {
@@ -2504,8 +2516,22 @@ void AccessibilityRenderObject::handleActiveDescendantChanged()
     if (!renderer()->frame().selection().isFocusedAndActive() || renderer()->document().focusedElement() != element)
         return;
 
-    if (activeDescendant() && shouldNotifyActiveDescendant())
-        renderer()->document().axObjectCache()->postNotification(renderer(), AXObjectCache::AXActiveDescendantChanged);
+    auto* activeDescendant = this->activeDescendant();
+    if (activeDescendant && shouldNotifyActiveDescendant()) {
+        auto* targetRenderer = renderer();
+        
+#if PLATFORM(COCOA)
+        // If the combobox's activeDescendant is inside another object, the target element should be that parent.
+        if (isComboBox()) {
+            if (auto* ariaOwner = targetElementForActiveDescendant(aria_ownsAttr, activeDescendant))
+                targetRenderer = ariaOwner;
+            else if (auto* ariaController = targetElementForActiveDescendant(aria_controlsAttr, activeDescendant))
+                targetRenderer = ariaController;
+        }
+#endif
+    
+        renderer()->document().axObjectCache()->postNotification(targetRenderer, AXObjectCache::AXActiveDescendantChanged);
+    }
 }
 
 AccessibilityObject* AccessibilityRenderObject::correspondingControlForLabelElement() const
@@ -3299,6 +3325,7 @@ bool AccessibilityRenderObject::canHaveSelectedChildren() const
     case AccessibilityRole::TabList:
     case AccessibilityRole::Tree:
     case AccessibilityRole::TreeGrid:
+    case AccessibilityRole::List:
     // These roles are containers whose children are treated as selected by assistive
     // technologies. We can get the "selected" item via aria-activedescendant or the
     // focused element.
@@ -3326,7 +3353,7 @@ void AccessibilityRenderObject::ariaSelectedRows(AccessibilityChildrenVector& re
     // Get all the rows.
     auto rowsIteration = [&](auto& rows) {
         for (auto& row : rows) {
-            if (row->isSelected()) {
+            if (row->isSelected() || row->isActiveDescendantOfFocusedContainer()) {
                 result.append(row);
                 if (!isMulti)
                     break;
@@ -3350,7 +3377,7 @@ void AccessibilityRenderObject::ariaListboxSelectedChildren(AccessibilityChildre
 
     for (const auto& child : children()) {
         // Every child should have aria-role option, and if so, check for selected attribute/state.
-        if (child->isSelected() && child->ariaRoleAttribute() == AccessibilityRole::ListBoxOption) {
+        if (child->ariaRoleAttribute() == AccessibilityRole::ListBoxOption && (child->isSelected() || child->isActiveDescendantOfFocusedContainer())) {
             result.append(child);
             if (!isMulti)
                 return;
@@ -3379,6 +3406,10 @@ void AccessibilityRenderObject::selectedChildren(AccessibilityChildrenVector& re
         if (AccessibilityObject* selectedTab = selectedTabItem())
             result.append(selectedTab);
         return;
+    case AccessibilityRole::List:
+        if (auto* selectedListItemChild = selectedListItem())
+            result.append(selectedListItemChild);
+        return;
     case AccessibilityRole::Menu:
     case AccessibilityRole::MenuBar:
         if (AccessibilityObject* descendant = activeDescendant()) {
index 5ed42e7..135eb14 100644 (file)
@@ -285,6 +285,7 @@ private:
 
     bool shouldGetTextFromNode(AccessibilityTextUnderElementMode) const;
 
+    RenderObject* targetElementForActiveDescendant(const QualifiedName&, AccessibilityObject*) const;
     bool canHavePlainText() const;
 };
 
index a569e33..f94bad5 100644 (file)
@@ -271,13 +271,13 @@ void AXObjectCache::postPlatformNotification(AccessibilityObject* obj, AXNotific
     switch (notification) {
         case AXActiveDescendantChanged:
             // An active descendant change for trees means a selected rows change.
-            if (obj->isTree())
+            if (obj->isTree() || obj->isTable())
                 macNotification = NSAccessibilitySelectedRowsChangedNotification;
             
             // When a combobox uses active descendant, it means the selected item in its associated
             // list has changed. In these cases we should use selected children changed, because
             // we don't want the focus to change away from the combobox where the user is typing.
-            else if (obj->isComboBox())
+            else if (obj->isComboBox() || obj->isList() || obj->isListBox())
                 macNotification = NSAccessibilitySelectedChildrenChangedNotification;
             else
                 macNotification = NSAccessibilityFocusedUIElementChangedNotification;