Bug 151498 - AX: aria-owns attribute does not work as expected for role radio
Summary: AX: aria-owns attribute does not work as expected for role radio
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: Accessibility (show other bugs)
Version: Safari 9
Hardware: All All
: P2 Normal
Assignee: chris fleizach
URL:
Keywords: InRadar
: 151429 (view as bug list)
Depends on:
Blocks:
 
Reported: 2015-11-20 09:36 PST by Jonathan Dallas
Modified: 2023-04-04 00:55 PDT (History)
16 users (show)

See Also:


Attachments
Demo of the test case in the problem description (337 bytes, text/html)
2015-11-20 09:36 PST, Jonathan Dallas
no flags Details
Screenshot of demo test case. (second item selected) (386.91 KB, image/png)
2015-11-20 09:53 PST, Jonathan Dallas
no flags Details
Screenshot of demo test case. (third item selected) (386.13 KB, image/png)
2015-11-20 09:54 PST, Jonathan Dallas
no flags Details
Patch (12.58 KB, patch)
2023-01-03 00:15 PST, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (15.74 KB, patch)
2023-01-03 16:39 PST, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (16.99 KB, patch)
2023-01-31 01:07 PST, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (20.02 KB, patch)
2023-03-12 19:22 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (20.21 KB, patch)
2023-03-13 00:02 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (19.51 KB, patch)
2023-03-13 07:21 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (23.49 KB, patch)
2023-03-20 00:36 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (23.51 KB, patch)
2023-03-20 08:24 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (22.57 KB, patch)
2023-03-20 08:51 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (22.92 KB, patch)
2023-03-20 12:53 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (22.92 KB, patch)
2023-03-20 15:38 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (24.83 KB, patch)
2023-03-20 20:26 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (25.42 KB, patch)
2023-03-26 23:01 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (80.07 KB, patch)
2023-03-30 00:25 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (80.07 KB, patch)
2023-04-01 01:29 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (79.27 KB, patch)
2023-04-01 01:32 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (87.25 KB, patch)
2023-04-01 23:48 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (87.25 KB, patch)
2023-04-02 20:29 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (87.16 KB, patch)
2023-04-02 22:03 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (88.04 KB, patch)
2023-04-02 23:30 PDT, chris fleizach
no flags Details | Formatted Diff | Diff
Patch (87.29 KB, patch)
2023-04-03 15:48 PDT, chris fleizach
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Jonathan Dallas 2015-11-20 09:36:37 PST
Created attachment 265962 [details]
Demo of the test case in the problem description

When using the aria-owns attribute radio controls are not grouped as expected.

Here is a test case:
    <div role="radiogroup" aria-owns="r1 r2 r3">
       <div role="radio" id="r1">Alpha</div>
       <div role="radio" id="r2">Beta</div>
    </div>
    <!-- this one, too. -->
    <div role="radio" id="r3">Gamma</div>

In this case the 3rd radio button "Gamma" should be included in the radio group. However, when VoiceOver looks at this it only sees two radio buttons in the group, the 3rd is announced as a radio button without a group.

Spec for aria-owns: http://www.w3.org/TR/wai-aria-1.1/#aria-owns
Comment 1 Radar WebKit Bug Importer 2015-11-20 09:37:15 PST
<rdar://problem/23630121>
Comment 2 Jonathan Dallas 2015-11-20 09:41:56 PST
*** Bug 151429 has been marked as a duplicate of this bug. ***
Comment 3 Jonathan Dallas 2015-11-20 09:53:43 PST
Created attachment 265964 [details]
Screenshot of demo test case. (second item selected)
Comment 4 Jonathan Dallas 2015-11-20 09:54:16 PST
Created attachment 265965 [details]
Screenshot of demo test case. (third item selected)
Comment 5 James Craig 2022-06-16 19:03:28 PDT
possibly forward dupe to bug 241694
Comment 6 chris fleizach 2023-01-03 00:15:57 PST
Created attachment 464300 [details]
Patch
Comment 7 chris fleizach 2023-01-03 16:39:03 PST
Created attachment 464316 [details]
Patch
Comment 8 Andres Gonzalez 2023-01-04 06:08:48 PST
(In reply to chris fleizach from comment #7)
> Created attachment 464316 [details]
> Patch

--- a/Source/WebCore/accessibility/AXObjectCache.cpp
+++ b/Source/WebCore/accessibility/AXObjectCache.cpp

+    if (relationType == AXRelationType::OwnerFor)
+        origin->setNeedsToUpdateChildren();
+    if (relationType == AXRelationType::OwnedBy)

else if

+        origin->parentObjectUnignored()->setNeedsToUpdateChildren();

origin->parentObjectUnignored() can be null.

--- a/Source/WebCore/accessibility/AccessibilityNodeObject.cpp
+++ b/Source/WebCore/accessibility/AccessibilityNodeObject.cpp

+void AccessibilityNodeObject::updatedOwnedChildren()
+{
+    // If we added any children who are owned by others, remove them from our list.
+    for (unsigned k = 0; k < m_children.size(); k++) {
+        auto child = m_children[k];
+        auto childOwners = child->owners();
+        if (childOwners.size() && !childOwners.contains(this)) {
+            m_children.remove(k);
+            k--;

You can do one liner:

+            m_children.remove(k--);

There is probably a more elegant way of doing this with reverse iterators or something like that.

What is this trying to do? To ensure that a child is owned only by one parent? If so, maybe we should do that check earlier as we add relations?
Comment 9 Andres Gonzalez 2023-01-04 06:27:51 PST
(In reply to chris fleizach from comment #7)
> Created attachment 464316 [details]
> Patch

--- a/Source/WebCore/accessibility/AccessibilityNodeObject.h
+++ b/Source/WebCore/accessibility/AccessibilityNodeObject.h


+    void updatedOwnedChildren();

Should this be final?

--- a/Source/WebCore/accessibility/AccessibilityObject.cpp
+++ b/Source/WebCore/accessibility/AccessibilityObject.cpp

-        ASSERT(isTableComponent(*child) || isTableComponent(*this) || child->parentObject() == this);
+        ASSERT(isTableComponent(*child) || isTableComponent(*this) || child->parentObject() == this || child->owners().contains(this));

At this point, shouldn't parentObject already return the owner if the child is indeed owned?

--- a/Source/WebCore/accessibility/AccessibilityObjectInterface.h
+++ b/Source/WebCore/accessibility/AccessibilityObjectInterface.h
@@ -1518,14 +1518,23 @@ namespace Accessibility {
 template<typename T, typename F>
 T* findAncestor(const T& object, bool includeSelf, const F& matches)

parentObject() should return the owner if the object is owned, otherwise we are going to need to do this everywhere we get a parent. After the aria-owns is processed and the hierarchy modified accordingly, we should work with that hierarchy and should not have to check for owners again.
Comment 10 chris fleizach 2023-01-04 11:35:28 PST
(In reply to Andres Gonzalez from comment #9)
> (In reply to chris fleizach from comment #7)
> > Created attachment 464316 [details]
> > Patch
> 
> --- a/Source/WebCore/accessibility/AccessibilityNodeObject.h
> +++ b/Source/WebCore/accessibility/AccessibilityNodeObject.h
> 
> 
> +    void updatedOwnedChildren();
> 
> Should this be final?

These ones aren't virtual, so can't do it:
    error: only virtual member functions can be marked 'final'
Comment 11 chris fleizach 2023-01-31 01:07:51 PST
Created attachment 464778 [details]
Patch
Comment 12 chris fleizach 2023-03-12 19:22:52 PDT
Created attachment 465412 [details]
Patch
Comment 13 chris fleizach 2023-03-13 00:02:51 PDT
Created attachment 465414 [details]
Patch
Comment 14 chris fleizach 2023-03-13 07:21:16 PDT
Created attachment 465419 [details]
Patch
Comment 15 chris fleizach 2023-03-20 00:36:25 PDT
Created attachment 465515 [details]
Patch
Comment 16 chris fleizach 2023-03-20 08:24:34 PDT
Created attachment 465521 [details]
Patch
Comment 17 Andres Gonzalez 2023-03-20 08:26:12 PDT
(In reply to chris fleizach from comment #15)
> Created attachment 465515 [details]
> Patch

--- a/Source/WebCore/accessibility/AXObjectCache.cpp
+++ b/Source/WebCore/accessibility/AXObjectCache.cpp

+AccessibilityObject* AXObjectCache::get(AXID objectID)
+{
+    return objectID ? m_objects.get(objectID) : nullptr;
+}

We already have:
    AccessibilityObject* objectForID(const AXID id) const { return m_objects.get(id); }

Which matches the equivalent in AXIsolatedTree.

I'm fine with renaming to just get(...), but we should then rename the AXIsolatedTree method. We also have objectsForIDs(const Vector<AXID>& axIDs) const.
Comment 18 chris fleizach 2023-03-20 08:29:00 PDT
> 
> Which matches the equivalent in AXIsolatedTree.
> 
> I'm fine with renaming to just get(...), but we should then rename the
> AXIsolatedTree method. We also have objectsForIDs(const Vector<AXID>& axIDs)
> const.

Changing to objectForID
Comment 19 chris fleizach 2023-03-20 08:51:08 PDT
Created attachment 465522 [details]
Patch
Comment 20 Andres Gonzalez 2023-03-20 09:54:13 PDT
(In reply to chris fleizach from comment #19)
> Created attachment 465522 [details]
> Patch

--- a/Source/WebCore/accessibility/AXObjectCache.cpp
+++ b/Source/WebCore/accessibility/AXObjectCache.cpp

-    if (AccessibilityObject* scrollViewObject = get(view)) {
+    if (AccessibilityObject* scrollViewObject = get(&view)) {

AccessibilityObject* --> auto*

--- a/Source/WebCore/accessibility/AXObjectCache.h
+++ b/Source/WebCore/accessibility/AXObjectCache.h


+    void deferScrollbarUpdate(ScrollView*);

We are favoring the onScrollbarUpdate naming convention instead of defer***() for event notification methods called from outside accessibility.
Comment 21 chris fleizach 2023-03-20 12:53:04 PDT
Created attachment 465527 [details]
Patch
Comment 22 chris fleizach 2023-03-20 15:38:58 PDT
Created attachment 465529 [details]
Patch
Comment 23 Tyler Wilcock 2023-03-20 18:35:05 PDT
View in context: https://bugs.webkit.org/attachment.cgi?id=465529&action=review

Do you know if we have any existing layout tests that verify aria-owns cycles don't cause us to crash? Some ideas:

A aria-owns B.
B aria-owns A.

Results: Neither B nor A have any children, their parent is the web area (or whatever)

Another:

A aria-owns B.
B DOM-owns C.
C aria-owns A.

Results: A has child B, B has child C, C has no children

> Source/WebCore/accessibility/AXObjectCache.cpp:4046
> +static bool validateOwnerCycleAbsence(AccessibilityObject* origin, AccessibilityObject* target, AXRelationType relationType)

I wonder if there is a more fitting method for this name considering it returns a bool. "relationCausesCycle"? "relationAbsentOfCycle"?

> Source/WebCore/accessibility/AXObjectCache.cpp:4098
> +            if (auto oldOwner = objectForID(oldOwnerIterator->key))

I believe AXObjectCache::objectForID returns an AccessibilityObject*, so this should be auto* rather than auto.

> Source/WebCore/accessibility/AXObjectCache.cpp:4099
> +                oldOwner->setNeedsToUpdateChildren();

Is there a reason you're calling `setNeedsToUpdateChildren` directly rather than using AXObjectCache::childrenChanged? There are hooks in that method to do things when children change that we probably want in this situation too (e.g. re-computing table exposure status if necessary).

> Source/WebCore/accessibility/AXObjectCache.cpp:4104
> +        origin->parentObjectUnignored()->setNeedsToUpdateChildren();

Is it safe not to null-check parentObjectUnignored() here?

> Source/WebCore/accessibility/AccessibilityNodeObject.cpp:193
> +    auto owners = this->owners();

Does it make sense to ASSERT(owners.size() <= 1)? That could only be caused by a logic bug, right?

> Source/WebCore/accessibility/AccessibilityNodeObject.cpp:479
> +    for (auto child : ownedObjects()) {

auto evalutes to RefPtr here. I think we've been moving towards not using auto for smart pointers because their semantics are important to understand, so it's helpful to not hide them with auto.

> Source/WebCore/accessibility/AccessibilityNodeObject.cpp:483
> +        if (m_children.contains(child))
> +            m_children.removeFirst(child);

This does two iterations of m_children. Would it be correct to just unconditionally `m_children.removeFirst(child)`?

> Source/WebCore/accessibility/AccessibilityRenderObject.cpp:3098
> +        if (object.owners().size() && !object.owners().contains(this))

AXCoreObject::owners returns a copy, and we call it twice in a row here. Hopefully the compiler would be smart enough to optimize that away, but might be better to put `object.owners()` into a temporary local just in case.

> LayoutTests/accessibility/aria-owns-hierarchy-remap.html:4
> +<script src="../resources/js-test-pre.js"></script>

This should be replaced with "js-test.js".

> LayoutTests/accessibility/aria-owns-hierarchy-remap.html:26
> +<p id="description"></p>
> +<div id="console"></div>

This can be removed after importing js-test.js.

> LayoutTests/accessibility/aria-owns-hierarchy-remap.html:47
> +    debug(output);

`debug(output)` should go right before finishJSTest();

> LayoutTests/accessibility/aria-owns-hierarchy-remap.html:56
> +        await expectAsyncExpression("ax.accessibleElementById('list3').childrenCount", 0);
> +        await expectAsyncExpression("ax.accessibleElementById('list1').childrenCount", 3);
> +
> +        await expectAsyncExpression("ax.accessibleElementById('list1').childAtIndex(1).isEqual(ax.accessibleElementById('item5'))", "true");

These uses of `await expectAsyncExpression` should be replaced by `output += await expectAsync(...);`

> LayoutTests/accessibility/aria-owns-hierarchy-remap.html:63
> +<script src="../resources/js-test-post.js"></script>

This can be removed if you import "js-test.js"
Comment 24 Tyler Wilcock 2023-03-20 18:42:37 PDT
> A aria-owns B.
> B aria-owns A.
> 
> Results: Neither B nor A have any children, their parent is the web area (or
> whatever)
Ah, I take this back, A should have B as a child in this situation
Comment 25 chris fleizach 2023-03-20 20:06:42 PDT
(In reply to Tyler Wilcock from comment #23)
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=465529&action=review
> 
> Do you know if we have any existing layout tests that verify aria-owns
> cycles don't cause us to crash? Some ideas:
> 
> A aria-owns B.
> B aria-owns A.
> 
> Results: Neither B nor A have any children, their parent is the web area (or
> whatever)
> 
> Another:
> 
> A aria-owns B.
> B DOM-owns C.
> C aria-owns A.
> 
> Results: A has child B, B has child C, C has no children
> 

Will add some more tests

> 
> > Source/WebCore/accessibility/AXObjectCache.cpp:4099
> > +                oldOwner->setNeedsToUpdateChildren();
> 
> Is there a reason you're calling `setNeedsToUpdateChildren` directly rather
> than using AXObjectCache::childrenChanged? There are hooks in that method to
> do things when children change that we probably want in this situation too
> (e.g. re-computing table exposure status if necessary).
> 

This naming seems unfortunate. From my usage, I don't know if the childrenChanged, but I do know that it should update the children. This is similar to [CALayer setNeedsDisplay] vs [CALayer display]

/* Reload the content of this layer. Calls the -drawInContext: method
 * then updates the `contents' property of the layer. Typically this is
 * not called directly. */
- (void)display;
Comment 26 chris fleizach 2023-03-20 20:26:11 PDT
Created attachment 465532 [details]
Patch
Comment 27 Tyler Wilcock 2023-03-20 22:45:12 PDT
Comment on attachment 465532 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=465532&action=review

> LayoutTests/accessibility/aria-owns-hierarchy-remap.html:53
> +        output += await expectAsyncExpression("ax.accessibleElementById('list3').childrenCount", 0);
> +        output += await expectAsyncExpression("ax.accessibleElementById('list1').childrenCount", 3);
> +
> +        output += await expectAsyncExpression("ax.accessibleElementById('list1').childAtIndex(1).isEqual(ax.accessibleElementById('item5'))", "true");

Typo -- it's "expectAsync", not "expectAsyncExpression".

output += await expectAsync("ax.accessibleElementById('list3').childrenCount", 0);

As the person who introduced both of those functions — sorry for the confusion. It has been on my to-do list to remove "expectAsyncExpression" since it forces the output to be debugged, which is not good for the semantics of our tests.
Comment 28 Tyler Wilcock 2023-03-20 22:45:23 PDT
View in context: https://bugs.webkit.org/attachment.cgi?id=465532&action=review

> LayoutTests/accessibility/aria-owns-hierarchy-remap.html:53
> +        output += await expectAsyncExpression("ax.accessibleElementById('list3').childrenCount", 0);
> +        output += await expectAsyncExpression("ax.accessibleElementById('list1').childrenCount", 3);
> +
> +        output += await expectAsyncExpression("ax.accessibleElementById('list1').childAtIndex(1).isEqual(ax.accessibleElementById('item5'))", "true");

Typo -- it's "expectAsync", not "expectAsyncExpression".

output += await expectAsync("ax.accessibleElementById('list3').childrenCount", 0);

As the person who introduced both of those functions — sorry for the confusion. It has been on my to-do list to remove "expectAsyncExpression" since it forces the output to be debugged, which is not good for the semantics of our tests.
Comment 29 chris fleizach 2023-03-26 23:01:27 PDT
Created attachment 465603 [details]
Patch
Comment 30 chris fleizach 2023-03-30 00:25:07 PDT
Created attachment 465675 [details]
Patch
Comment 31 chris fleizach 2023-04-01 01:29:48 PDT
Created attachment 465723 [details]
Patch
Comment 32 chris fleizach 2023-04-01 01:32:11 PDT
Created attachment 465724 [details]
Patch
Comment 33 chris fleizach 2023-04-01 23:48:57 PDT
Created attachment 465732 [details]
Patch
Comment 34 chris fleizach 2023-04-02 20:29:20 PDT
Created attachment 465741 [details]
Patch
Comment 35 chris fleizach 2023-04-02 22:03:15 PDT
Created attachment 465744 [details]
Patch
Comment 36 chris fleizach 2023-04-02 23:30:58 PDT
Created attachment 465745 [details]
Patch
Comment 37 Tyler Wilcock 2023-04-03 14:12:31 PDT
Comment on attachment 465745 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=465745&action=review

> Source/WebCore/accessibility/AXObjectCache.cpp:4101
> +        origin->setNeedsToUpdateChildren();

Here's another use of `setNeedsToUpdateChildren` over `AXObjectCache::childrenChanged`. Is this intentional? I agree the naming and relationship between these methods is confusing and needs to be fixed in the future.

> LayoutTests/accessibility/aria-checkbox-sends-notification-expected.txt:11
> +FAIL: Timed out waiting for notifyDone to be called
> +
> +Test Checkbox
> +This tests that checking of an aria checkbox sends a notification.
> +
> +On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
> +
> +
> +Got notification: AXValueChanged
> +Got notification: AXValueChanged
> +

Seems like this test is failing.

> LayoutTests/accessibility/element-reflection-ariaactivedescendant.html:29
> +       	output += expect("axTarget1.selectedChildAtIndex(0).isEqual(axDescendant1)", "true");

Seems like this indentation might not match the sibling statements.

> LayoutTests/accessibility/element-reflection-ariacontrols.html:72
> +setTimeout(function() {

This is nested within the other setTimeouts, right? If so, seems like the indentation is wrong — should be three levels indented.

> LayoutTests/accessibility/element-reflection-ariacontrols.html:75
> +         axPanel3 = accessibilityController.accessibleElementById("panel3");

Extra space preceding "axPanel3".

> LayoutTests/accessibility/element-reflection-ariacontrols.html:95
> +       }, 0);

This indentation doesn't seem right. Each "}, 0);" and the contents within should be indented one level deeper than the one encapsulating it.

> LayoutTests/accessibility/element-reflection-ariaowns.html:69
> +        if (accessibilityController.platformName == "mac") {

The braces for this if-else aren't necessary.
Comment 38 chris fleizach 2023-04-03 14:16:32 PDT
(In reply to Tyler Wilcock from comment #37)
> Comment on attachment 465745 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=465745&action=review
> 
> > Source/WebCore/accessibility/AXObjectCache.cpp:4101
> > +        origin->setNeedsToUpdateChildren();
> 
> Here's another use of `setNeedsToUpdateChildren` over
> `AXObjectCache::childrenChanged`. Is this intentional? I agree the naming
> and relationship between these methods is confusing and needs to be fixed in
> the future.

Should be able to change. Just missed this from last review
> 
> > LayoutTests/accessibility/aria-checkbox-sends-notification-expected.txt:11
> > +FAIL: Timed out waiting for notifyDone to be called
> > +
> > +Test Checkbox
> > +This tests that checking of an aria checkbox sends a notification.
> > +
> > +On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
> > +
> > +
> > +Got notification: AXValueChanged
> > +Got notification: AXValueChanged
> > +
> 
> Seems like this test is failing.
> 
> > LayoutTests/accessibility/element-reflection-ariaactivedescendant.html:29
> > +       	output += expect("axTarget1.selectedChildAtIndex(0).isEqual(axDescendant1)", "true");
> 
> Seems like this indentation might not match the sibling statements.
> 
> > LayoutTests/accessibility/element-reflection-ariacontrols.html:72
> > +setTimeout(function() {
> 
> This is nested within the other setTimeouts, right? If so, seems like the
> indentation is wrong — should be three levels indented.
> 
> > LayoutTests/accessibility/element-reflection-ariacontrols.html:75
> > +         axPanel3 = accessibilityController.accessibleElementById("panel3");
> 
> Extra space preceding "axPanel3".
> 
> > LayoutTests/accessibility/element-reflection-ariacontrols.html:95
> > +       }, 0);
> 
> This indentation doesn't seem right. Each "}, 0);" and the contents within
> should be indented one level deeper than the one encapsulating it.
> 
> > LayoutTests/accessibility/element-reflection-ariaowns.html:69
> > +        if (accessibilityController.platformName == "mac") {
> 
> The braces for this if-else aren't necessary.
Comment 39 chris fleizach 2023-04-03 15:48:13 PDT
Created attachment 465755 [details]
Patch
Comment 40 EWS 2023-04-03 23:31:39 PDT
Committed 262566@main (ca304bdacc08): <https://commits.webkit.org/262566@main>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 465755 [details].
Comment 41 Ross Kirsling 2023-04-04 00:55:34 PDT
Landed build fix: https://github.com/WebKit/WebKit/pull/12368