Bug 260621 - AX: aria-controls-with-tabs fails in ITM
Summary: AX: aria-controls-with-tabs fails in ITM
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: Accessibility (show other bugs)
Version: WebKit Nightly Build
Hardware: All All
: P2 Normal
Assignee: Nobody
URL:
Keywords: InRadar
Depends on:
Blocks:
 
Reported: 2023-08-23 11:09 PDT by Joshua Hoffman
Modified: 2023-08-29 20:04 PDT (History)
12 users (show)

See Also:


Attachments
Patch (6.94 KB, patch)
2023-08-23 11:40 PDT, Joshua Hoffman
no flags Details | Formatted Diff | Diff
Patch (8.67 KB, patch)
2023-08-24 09:05 PDT, Joshua Hoffman
no flags Details | Formatted Diff | Diff
Patch (6.68 KB, patch)
2023-08-25 11:37 PDT, Joshua Hoffman
no flags Details | Formatted Diff | Diff
Patch (6.38 KB, patch)
2023-08-28 11:47 PDT, Joshua Hoffman
no flags Details | Formatted Diff | Diff
Patch (6.38 KB, patch)
2023-08-28 12:19 PDT, Joshua Hoffman
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Joshua Hoffman 2023-08-23 11:09:05 PDT
The aria-controls-with-tabs.html test fails when run in isolated tree mode.
Comment 1 Radar WebKit Bug Importer 2023-08-23 11:09:16 PDT
<rdar://problem/114334241>
Comment 2 Joshua Hoffman 2023-08-23 11:40:33 PDT
Created attachment 467398 [details]
Patch
Comment 3 chris fleizach 2023-08-23 11:54:51 PDT
Comment on attachment 467398 [details]
Patch

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

> Source/WebCore/accessibility/AXObjectCache.cpp:1537
> +void AXObjectCache::handleTabOrControlledTabPanelSelected(Node* node)

feel like our naming has been moving to

onTabSelectionChange()

(that may be enough rather than tabOrControlledTab which is pretty wordy)

> Source/WebCore/accessibility/AXObjectCache.cpp:1647
> +    handleTabOrControlledTabPanelSelected(newNode);

can we make this method take old and new and do the work once?

> Source/WebCore/accessibility/AXObjectCache.cpp:4081
> +            if (notification.first->isTabList())

in this tabList case, we're not actually updating isSelected right? we just want SelectedChildren to change?
Comment 4 Joshua Hoffman 2023-08-23 12:01:45 PDT
(In reply to chris fleizach from comment #3)
> Comment on attachment 467398 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=467398&action=review
> 
> > Source/WebCore/accessibility/AXObjectCache.cpp:1647
> > +    handleTabOrControlledTabPanelSelected(newNode);
> 
> can we make this method take old and new and do the work once?

Yes, however we will have to do the traversal twice inside the method regardless in case the tab panels are controlled by different tabs. 

> > Source/WebCore/accessibility/AXObjectCache.cpp:4081
> > +            if (notification.first->isTabList())
> 
> in this tabList case, we're not actually updating isSelected right? we just
> want SelectedChildren to change?

Yes—I can handle AXChildrenChanged to update that property then (right now, we just do a full updateNode whenever that notification is received.
Comment 5 Tyler Wilcock 2023-08-23 12:04:18 PDT
Comment on attachment 467398 [details]
Patch

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

> Source/WebCore/accessibility/AXObjectCache.cpp:1542
> +    RefPtr<AccessibilityObject> object = getOrCreate(node);

I think this can just be RefPtr object = ... (no <> necessary)

> Source/WebCore/accessibility/AXObjectCache.cpp:1544
> +    if (object->isTabItem()) {

Do we need to null-check `object` before dereferencing here?

> Source/WebCore/accessibility/AXObjectCache.cpp:1558
> +    auto controllers = focusedControlledPanelItem->controllers();
> +
> +    for (auto& axObject : controllers)

Optional nit: this newline feels unnecessary to me.

> Source/WebCore/accessibility/AXObjectCache.cpp:1645
> +    // FIXME: optimize with ancestor flags to determine if an element is in a tabpanel

I feel like this could be written more clearly for future us...isn't the idea more like "FIXME: Consider creating a new ancestor flag to only do this work when |oldNode| or |newNode| have a tab panel ancestor (as that's the only time it is necessary)"? Or is that not accurate?

> Source/WebCore/accessibility/AXObjectCache.cpp:1685
>      else if (is<HTMLOptionElement>(node))
>          postNotification(node, AXSelectedStateChanged);
> +    else if (nodeHasRole(node, "tab"_s))
> +        postNotification(node, AXSelectedStateChanged);

Can we combine this with the previous else if rather than adding a new case?

else if (is<HTMLOptionElement>(node) || nodeHasRole(node, "tab"_s))

> Source/WebCore/accessibility/AXObjectCache.h:570
> +    void handleTabOrControlledTabPanelSelected(Node*);

Any time we add a method here, we need to make sure we add a no-op method to the !ENABLE(ACCESSIBILITY) section of this file to avoid breaking the Playstation build. See `handleRoleChanged` as an example.
Comment 6 Tyler Wilcock 2023-08-23 12:07:03 PDT
(In reply to chris fleizach from comment #3)
> Comment on attachment 467398 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=467398&action=review
> 
> > Source/WebCore/accessibility/AXObjectCache.cpp:1537
> > +void AXObjectCache::handleTabOrControlledTabPanelSelected(Node* node)
> 
> feel like our naming has been moving to
> 
> onTabSelectionChange()
> 
> (that may be enough rather than tabOrControlledTab which is pretty wordy)
The "onFooBar" naming scheme is only for events triggered from the DOM or render tree, so it doesn't fit this case. e.g. onTitleChange, onPopoverTargetToggle
Comment 7 Joshua Hoffman 2023-08-23 13:46:04 PDT
(In reply to Tyler Wilcock from comment #5)
> Comment on attachment 467398 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=467398&action=review
> 
> > Source/WebCore/accessibility/AXObjectCache.cpp:1645
> > +    // FIXME: optimize with ancestor flags to determine if an element is in a tabpanel
> 
> I feel like this could be written more clearly for future us...isn't the
> idea more like "FIXME: Consider creating a new ancestor flag to only do this
> work when |oldNode| or |newNode| have a tab panel ancestor (as that's the
> only time it is necessary)"? Or is that not accurate?
> 

Yes, that wording is more clear—I'll update that. 

> > Source/WebCore/accessibility/AXObjectCache.cpp:1685
> >      else if (is<HTMLOptionElement>(node))
> >          postNotification(node, AXSelectedStateChanged);
> > +    else if (nodeHasRole(node, "tab"_s))
> > +        postNotification(node, AXSelectedStateChanged);
> 
> Can we combine this with the previous else if rather than adding a new case?

Yep.
 
> else if (is<HTMLOptionElement>(node) || nodeHasRole(node, "tab"_s))
> 
> > Source/WebCore/accessibility/AXObjectCache.h:570
> > +    void handleTabOrControlledTabPanelSelected(Node*);
> 
> Any time we add a method here, we need to make sure we add a no-op method to
> the !ENABLE(ACCESSIBILITY) section of this file to avoid breaking the
> Playstation build. See `handleRoleChanged` as an example.

Got it!
Comment 8 Joshua Hoffman 2023-08-23 13:46:56 PDT
(In reply to Tyler Wilcock from comment #6)
> (In reply to chris fleizach from comment #3)
> > Comment on attachment 467398 [details]
> > Patch
> > 
> > View in context:
> > https://bugs.webkit.org/attachment.cgi?id=467398&action=review

> The "onFooBar" naming scheme is only for events triggered from the DOM or render tree, so it doesn't fit this case. e.g. onTitleChange, onPopoverTargetToggle

I could remove the word "controlled" from the method name to make it less wordy: handleTabOrTabPanelSelected ?
Comment 9 Joshua Hoffman 2023-08-24 09:05:00 PDT
Created attachment 467419 [details]
Patch
Comment 10 Tyler Wilcock 2023-08-24 18:37:27 PDT
Comment on attachment 467419 [details]
Patch

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

> Source/WebCore/accessibility/AXObjectCache.cpp:-4112
> -        case AXSelectedChildrenChanged:

Sorry to jump on this so late after Chris' comment, but the reason that AXSelectedChildrenChanged didn't just do an updateNodeProperty(*notification.first, AXPropertyName::SelectedChildren) is because selected children determines the `accessibleNameForNode` for listboxes:

// The Accname specification states that if the name is being calculated for a combobox
// or listbox inside a labeling element, return the text alternative of the chosen option.
AccessibilityObject::AccessibilityChildrenVector selectedChildren;
if (axObject->isListBox())
    selectedChildren = axObject->selectedChildren();

And in turn, the accessibleNameForNode is a transitive component of several other properties. And whenever a property's dependency graph gets complex (as in this case), we fallback to updateNode to make sure everything is up-to-date.

I'm guessing we don't have a testcase for this specific condition (we should add one if not).
Comment 11 Tyler Wilcock 2023-08-24 18:42:26 PDT
Comment on attachment 467419 [details]
Patch

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

> Source/WebCore/accessibility/AXObjectCache.cpp:1559
> +                for (auto& axObject : oldControllers) {

Optional nit, but I think `for (auto& oldController : oldControllers) {` would read better here.

> Source/WebCore/accessibility/AXObjectCache.cpp:1561
> +                    if (auto* parent = axObject->parentObject(); parent->isTabList())

You need to null-check this here when using multi-conditional if-statements. So it should be:

parent && parent->isTabList()

> Source/WebCore/accessibility/AXObjectCache.cpp:1571
> +    if (!newNode)
> +        return;
> +
> +    RefPtr newObject = get(newNode);

I think we can rely on AXObjectCache::get returning nullptr if given a nullptr newNode, so can probably remove the newNode null-check.

> Source/WebCore/accessibility/AXObjectCache.cpp:1588
> +    for (auto& axObject : newControllers) {

Optional nit, but I think `for (auto& newController : newControllers) {` would read better here.

> Source/WebCore/accessibility/AXObjectCache.cpp:1590
> +        if (auto* parent = axObject->parentObject(); parent->isTabList())

Also need a null-check here.
Comment 12 Andres Gonzalez 2023-08-25 04:53:17 PDT
(In reply to Tyler Wilcock from comment #5)
> Comment on attachment 467398 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=467398&action=review
> 
> 
> > Source/WebCore/accessibility/AXObjectCache.h:570
> > +    void handleTabOrControlledTabPanelSelected(Node*);
> 
> Any time we add a method here, we need to make sure we add a no-op method to
> the !ENABLE(ACCESSIBILITY) section of this file to avoid breaking the
> Playstation build. See `handleRoleChanged` as an example.

Only for those methods that are actually used when !ENABLE(ACCESSIBILITY), i.e., calls from outside accessibility. No need to have no-op stubs for private or methods that are only used inside accessibility.
Comment 13 Andres Gonzalez 2023-08-25 04:55:56 PDT
(In reply to Joshua Hoffman from comment #7)
> (In reply to Tyler Wilcock from comment #5)
> > Comment on attachment 467398 [details]
> > Patch
> > 
> > View in context:
> > https://bugs.webkit.org/attachment.cgi?id=467398&action=review
> > 
> > > Source/WebCore/accessibility/AXObjectCache.cpp:1645
> > > +    // FIXME: optimize with ancestor flags to determine if an element is in a tabpanel
> > 
> > I feel like this could be written more clearly for future us...isn't the
> > idea more like "FIXME: Consider creating a new ancestor flag to only do this
> > work when |oldNode| or |newNode| have a tab panel ancestor (as that's the
> > only time it is necessary)"? Or is that not accurate?
> > 
> 
> Yes, that wording is more clear—I'll update that. 
> 
> > > Source/WebCore/accessibility/AXObjectCache.cpp:1685
> > >      else if (is<HTMLOptionElement>(node))
> > >          postNotification(node, AXSelectedStateChanged);
> > > +    else if (nodeHasRole(node, "tab"_s))
> > > +        postNotification(node, AXSelectedStateChanged);
> > 
> > Can we combine this with the previous else if rather than adding a new case?
> 
> Yep.
>  
> > else if (is<HTMLOptionElement>(node) || nodeHasRole(node, "tab"_s))
> > 
> > > Source/WebCore/accessibility/AXObjectCache.h:570
> > > +    void handleTabOrControlledTabPanelSelected(Node*);
> > 
> > Any time we add a method here, we need to make sure we add a no-op method to
> > the !ENABLE(ACCESSIBILITY) section of this file to avoid breaking the
> > Playstation build. See `handleRoleChanged` as an example.
> 
> Got it!

See my comment above. Only do that when needed.
Comment 14 Andres Gonzalez 2023-08-25 07:22:15 PDT
(In reply to Joshua Hoffman from comment #9)
> Created attachment 467419 [details]
> Patch

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

+        else if (oldObject) {
+            RefPtr oldFocusedControlledPanelItem = Accessibility::findAncestor<AccessibilityObject>(*oldObject, false, [] (auto& axObject) {

oldFocusedControlledPanelItem --> oldFocusedControlledPanel
axObject --> ancestor

+                return axObject.roleValue() == AccessibilityRole::TabPanel && axObject.controllers().size();
+            });
+
+            if (oldFocusedControlledPanelItem) {
+                auto oldControllers = oldFocusedControlledPanelItem->controllers();

I think that in findAncestor, you only need to find the closest panel ancestor, not a panel with controllers. Then if you do find a panel, you can check the controllers and update them.

The rest of the function does the same thing for the new focus. Avoid the code duplication, maybe using a lambda. Also account for the case where the old panel and the new panel are the same, in which case you only need to check for controllers and do updates and notifications only once.

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

+    void handleTabOrTabPanelSelected(Node*, Node*);

What about updateTabs / updateTabPanels for name?

+inline void AXObjectCache::handleTabOrTabPanelSelected(Node*, Node*) { }

No needed since this is a private method.
Comment 15 Joshua Hoffman 2023-08-25 08:46:44 PDT
Comment on attachment 467419 [details]
Patch

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

>> Source/WebCore/accessibility/AXObjectCache.cpp:-4112
>> -        case AXSelectedChildrenChanged:
> 
> Sorry to jump on this so late after Chris' comment, but the reason that AXSelectedChildrenChanged didn't just do an updateNodeProperty(*notification.first, AXPropertyName::SelectedChildren) is because selected children determines the `accessibleNameForNode` for listboxes:
> 
> // The Accname specification states that if the name is being calculated for a combobox
> // or listbox inside a labeling element, return the text alternative of the chosen option.
> AccessibilityObject::AccessibilityChildrenVector selectedChildren;
> if (axObject->isListBox())
>     selectedChildren = axObject->selectedChildren();
> 
> And in turn, the accessibleNameForNode is a transitive component of several other properties. And whenever a property's dependency graph gets complex (as in this case), we fallback to updateNode to make sure everything is up-to-date.
> 
> I'm guessing we don't have a testcase for this specific condition (we should add one if not).

Ah I see—will revert this to use updateNode. Let's definitely add that case in a follow-up. Thanks!
Comment 16 Joshua Hoffman 2023-08-25 08:49:32 PDT
(In reply to Andres Gonzalez from comment #14)
> (In reply to Joshua Hoffman from comment #9)
> > Created attachment 467419 [details]
> > Patch
> 
> +                return axObject.roleValue() == AccessibilityRole::TabPanel
> && axObject.controllers().size();
> +            });
> +
> +            if (oldFocusedControlledPanelItem) {
> +                auto oldControllers =
> oldFocusedControlledPanelItem->controllers();
> 
> I think that in findAncestor, you only need to find the closest panel
> ancestor, not a panel with controllers. Then if you do find a panel, you can
> check the controllers and update them.
> 
> The rest of the function does the same thing for the new focus. Avoid the
> code duplication, maybe using a lambda. Also account for the case where the
> old panel and the new panel are the same, in which case you only need to
> check for controllers and do updates and notifications only once.
> 

I will refactor that method, thanks for the suggestions!
Comment 17 Joshua Hoffman 2023-08-25 11:37:39 PDT
Created attachment 467434 [details]
Patch
Comment 18 Joshua Hoffman 2023-08-28 11:47:04 PDT
Created attachment 467463 [details]
Patch
Comment 19 Joshua Hoffman 2023-08-28 12:19:39 PDT
Created attachment 467467 [details]
Patch
Comment 20 EWS 2023-08-28 22:09:38 PDT
Committed 267391@main (b1049697ce8b): <https://commits.webkit.org/267391@main>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 467467 [details].
Comment 21 Darin Adler 2023-08-29 12:51:44 PDT
Comment on attachment 467467 [details]
Patch

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

Some post-landing style comments

> Source/WebCore/accessibility/AXObjectCache.cpp:1559
> +    RefPtr oldObject = get(oldNode);

Slight better style:

    if (RefPtr oldObject = get(oldNode))

> Source/WebCore/accessibility/AXObjectCache.cpp:1564
> +        oldFocusedControlledPanel = Accessibility::findAncestor<AccessibilityObject>(*oldObject, false, [] (auto& ancestor) {
> +            return ancestor.roleValue() == AccessibilityRole::TabPanel;
> +        });

Would be nice to share this "find panel" code too, just as we share the "update tab" code.
Comment 22 Joshua Hoffman 2023-08-29 20:04:01 PDT
Comment on attachment 467467 [details]
Patch

Thanks, Darin! I will make these updates in a follow-up