Bug 256950 - AX: display:contents lists don't return the correct subrole
Summary: AX: display:contents lists don't return the correct subrole
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: Accessibility (show other bugs)
Version: WebKit Nightly Build
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: Tyler Wilcock
URL:
Keywords: InRadar
Depends on:
Blocks: 239478
  Show dependency treegraph
 
Reported: 2023-05-17 23:23 PDT by Tyler Wilcock
Modified: 2023-06-22 14:09 PDT (History)
10 users (show)

See Also:


Attachments
Patch (28.29 KB, patch)
2023-05-17 23:35 PDT, Tyler Wilcock
no flags Details | Formatted Diff | Diff
Patch (28.42 KB, patch)
2023-05-20 11:26 PDT, Tyler Wilcock
no flags Details | Formatted Diff | Diff
Patch (32.92 KB, patch)
2023-05-20 11:31 PDT, Tyler Wilcock
no flags Details | Formatted Diff | Diff
Patch (34.23 KB, patch)
2023-05-20 14:16 PDT, Tyler Wilcock
no flags Details | Formatted Diff | Diff
Patch (35.65 KB, patch)
2023-05-24 12:01 PDT, Tyler Wilcock
no flags Details | Formatted Diff | Diff
Patch (36.38 KB, patch)
2023-05-26 18:28 PDT, Tyler Wilcock
no flags Details | Formatted Diff | Diff
Patch (36.38 KB, patch)
2023-05-28 12:59 PDT, Tyler Wilcock
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Tyler Wilcock 2023-05-17 23:23:34 PDT
This is important because some ATs on macOS do change their behavior based on the various list subrole types.
Comment 1 Radar WebKit Bug Importer 2023-05-17 23:23:49 PDT
<rdar://problem/109498423>
Comment 2 Tyler Wilcock 2023-05-17 23:35:02 PDT
Created attachment 466397 [details]
Patch
Comment 3 Andres Gonzalez 2023-05-19 08:09:14 PDT
(In reply to Tyler Wilcock from comment #2)
> Created attachment 466397 [details]
> Patch

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

+static bool shouldCreateAccessibilityList(Node* node)

I would call it simply isAXList(...).

+{
+    // If the node is aria role="list" or the aria role is empty and its a

Typo: its -> it's

+    // ul/ol/dl type (it shouldn't be a list if aria says otherwise).
+    return (node && ((nodeHasRole(node, "list"_s) || nodeHasRole(node, "directory"_s))
+        || (nodeHasRole(node, nullAtom()) && (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(dlTag) || node->hasTagName(menuTag)))));
+}

The role attribute should not override <ul>, <ol>, <dl> tag names. A <ul> element should be a list.
Although I see that this was there in createObjectFromRenderer before this change, it is incorrect. Can we add a FIXME for this?

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

+    AccessibilityRole determineAccessibilityRoleInner();

Don't see where this is defined or used. Missed to remove?

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

 bool AccessibilityNodeObject::canHaveChildren() const
 {
-    // If this is an AccessibilityRenderObject, then it's okay if this object
-    // doesn't have a node - there are some renderers that don't have associated
-    // nodes, like scroll areas and css-generated text.
-    if (!node() && !isAccessibilityRenderObject())
+    if (!renderer() && !node())
         return false;

I find this comment somewhat instructive if it holds true, that there can be RenderObjects with no Node. Can we keep it, or a revision of it?
Furthermore, how can there be an object without eigher renderer or node? There is the AX mocked objects, but I don't think they derive from AXNodeObject, do they?

@@ -572,6 +588,8 @@ bool AccessibilityNodeObject::computeAccessibilityIsIgnored() const
     // it's been initialized.
     ASSERT(m_initialized);
 #endif
+    if (!node())
+        return true;

Why do we need this?

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

+AccessibilityRenderObject::AccessibilityRenderObject(Node& node)
+    : AccessibilityNodeObject(&node)
+    , m_renderer(node.renderer())
+{
+    // We should only ever create an instance of this class with a node if that node has no renderer (i.e. because of display:contents).
+    ASSERT(!node.renderer());
+}

Then we don't need

+    , m_renderer(node.renderer())

 Node* AccessibilityRenderObject::node() const

 do we need this override at all?

 {
     if (!m_renderer)
-        return nullptr;
-    if (m_renderer->isRenderView())
+        return AccessibilityNodeObject::node();
+    if (UNLIKELY(m_renderer->isRenderView()))

is this possible? Or a RenderView will always create an AXScrollView instead.

         return &m_renderer->document();
     return m_renderer->node();
 }


 AccessibilityObjectInclusion AccessibilityRenderObject::defaultObjectInclusion() const
 {
-    if (!m_renderer)
+    if (!m_renderer && !node())
         return AccessibilityObjectInclusion::IgnoreObject;

Is it possible an AXRenderObject with neigher renderer nor Node?

--- a/LayoutTests/platform/mac-wk2/accessibility/aria-visible-element-roles-expected.txt
+++ b/LayoutTests/platform/mac-wk2/accessibility/aria-visible-element-roles-expected.txt

-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup
+    AXSubrole: AXContentList

is this expected, the role changing from list to group?


 <ul hidden="" aria-hidden="false" id="ul"></ul>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup
+    AXSubrole: AXContentList

Dito.

 <menu hidden="" aria-hidden="false" type="toolbar" id="menu-toolbar"></menu>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup
+    AXSubrole: AXContentList

Dito.
Comment 4 Tyler Wilcock 2023-05-20 11:26:06 PDT
Created attachment 466438 [details]
Patch
Comment 5 Tyler Wilcock 2023-05-20 11:31:25 PDT
Created attachment 466439 [details]
Patch
Comment 6 Tyler Wilcock 2023-05-20 11:40:12 PDT
> +    // ul/ol/dl type (it shouldn't be a list if aria says otherwise).
> +    return (node && ((nodeHasRole(node, "list"_s) || nodeHasRole(node,
> "directory"_s))
> +        || (nodeHasRole(node, nullAtom()) && (node->hasTagName(ulTag) ||
> node->hasTagName(olTag) || node->hasTagName(dlTag) ||
> node->hasTagName(menuTag)))));
> +}
> 
> The role attribute should not override <ul>, <ol>, <dl> tag names. A <ul>
> element should be a list.
> Although I see that this was there in createObjectFromRenderer before this
> change, it is incorrect. Can we add a FIXME for this?
I think we should hold off adding a FIXME for this until we get agreement from standards folks that "role attribute should not override <ul>, <ol>, <dl> tag names" is a change that should be made.

> @@ -572,6 +588,8 @@ bool
> AccessibilityNodeObject::computeAccessibilityIsIgnored() const
>      // it's been initialized.
>      ASSERT(m_initialized);
>  #endif
> +    if (!node())
> +        return true;
> 
> Why do we need this?
The intention is to catch the case where the WeakPtr<Node> m_node has been destroyed but the AX object hasn't been cleaned up yet (similar to the check in AccessibilityRenderObject::computeAccessibilityIsIgnored).

>  Node* AccessibilityRenderObject::node() const
> 
>  do we need this override at all?
I did try removing it, but that causes many tests to fail. Maybe for cases where RenderObject::node() initially is nullptr when creating the object, but becomes not nullptr later? I have to imagine that would mess lots of other things up, though...so something to investigate for the future.

>  {
>      if (!m_renderer)
> -        return nullptr;
> -    if (m_renderer->isRenderView())
> +        return AccessibilityNodeObject::node();
> +    if (UNLIKELY(m_renderer->isRenderView()))
> 
> is this possible? Or a RenderView will always create an AXScrollView instead.
RenderViews are created as AccessibilityRenderObjects, not AccessibilityScrollView (that is ScrollView).

> AccessibilityRenderObject::defaultObjectInclusion() const
>  {
> -    if (!m_renderer)
> +    if (!m_renderer && !node())
>          return AccessibilityObjectInclusion::IgnoreObject;
> 
> Is it possible an AXRenderObject with neigher renderer nor Node?
The intention is to catch the case where the WeakPtr<RenderObject> m_renderer and WeakPtr<Node> m_node have been destroyed but that AX object hasn't been cleaned up yet.

> a/LayoutTests/platform/mac-wk2/accessibility/aria-visible-element-roles-
> expected.txt
> +++
> b/LayoutTests/platform/mac-wk2/accessibility/aria-visible-element-roles-
> expected.txt
> 
> -    AXRole: AXList
> -    computedRoleString: list
> +    AXRole: AXGroup
> +    AXSubrole: AXContentList
> 
> is this expected, the role changing from list to group?
> 
> 
>  <ul hidden="" aria-hidden="false" id="ul"></ul>
> -    AXRole: AXList
> -    computedRoleString: list
> +    AXRole: AXGroup
> +    AXSubrole: AXContentList
> 
> Dito.
> 
>  <menu hidden="" aria-hidden="false" type="toolbar" id="menu-toolbar"></menu>
> -    AXRole: AXList
> -    computedRoleString: list
> +    AXRole: AXGroup
> +    AXSubrole: AXContentList
> 
> Dito.
Yes, all intentional. Because we now properly create display:contents lists as AccessibilityList rather than AccessibilityNodeObject, the more stringent "is this a list" heuristics are applied, and "ARIA visible" lists do not pass that test (because nothing is visibly rendered).
Comment 7 Tyler Wilcock 2023-05-20 14:16:32 PDT
Created attachment 466440 [details]
Patch
Comment 8 Andres Gonzalez 2023-05-24 06:32:48 PDT
(In reply to Tyler Wilcock from comment #7)
> Created attachment 466440 [details]
> Patch

@@ -516,8 +535,9 @@ bool AccessibilityNodeObject::canHaveChildren() const
 {
     // If this is an AccessibilityRenderObject, then it's okay if this object
     // doesn't have a node - there are some renderers that don't have associated
-    // nodes, like scroll areas and css-generated text.
-    if (!node() && !isAccessibilityRenderObject())
+    // nodes, like scroll areas and css-generated text. However, if there also
+    // no renderer, we should bail.
+    if (!renderer() && !node())
         return false;

Can we really have this scenario? An AccessibilityNodeObject that has neither renderer nor node associated with it? If this is not a possible, expected state, maybe we should add an ASSERT here.

@@ -1064,7 +1049,7 @@ static AccessibilityObjectInclusion objectInclusionFromAltText(const String& alt

 AccessibilityObjectInclusion AccessibilityRenderObject::defaultObjectInclusion() const
 {
-    if (!m_renderer)
+    if (!m_renderer && !node())
         return AccessibilityObjectInclusion::IgnoreObject;

Same question here.

--- a/LayoutTests/platform/glib/accessibility/aria-visible-element-roles-expected.txt
+++ b/LayoutTests/platform/glib/accessibility/aria-visible-element-roles-expected.txt
@@ -105,12 +105,10 @@ This test ensures ARIA visible elements (e.g. those with both `hidden` and `aria
     AXRole: AXDescriptionList

 <ol hidden="" aria-hidden="false" id="ol"></ol>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup

 <ul hidden="" aria-hidden="false" id="ul"></ul>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup

Doesn't this mean we are breaking something for ATSPI? What it was a list before, now is a group.

@@ -180,8 +178,7 @@ This test ensures ARIA visible elements (e.g. those with both `hidden` and `aria
     computedRoleString: status

 <menu hidden="" aria-hidden="false" type="toolbar" id="menu-toolbar"></menu>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup

Dito.

--- a/LayoutTests/platform/mac-wk1/accessibility/aria-visible-element-roles-expected.txt
+++ b/LayoutTests/platform/mac-wk1/accessibility/aria-visible-element-roles-expected.txt
 <ol hidden="" aria-hidden="false" id="ol"></ol>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup
+    AXSubrole: AXContentList

 <ul hidden="" aria-hidden="false" id="ul"></ul>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup
+    AXSubrole: AXContentList

Dito.

@@ -203,8 +204,8 @@ This test ensures ARIA visible elements (e.g. those with both `hidden` and `aria
     AXSubrole: AXApplicationStatus

 <menu hidden="" aria-hidden="false" type="toolbar" id="menu-toolbar"></menu>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup
+    AXSubrole: AXContentList


Dito.

--- a/LayoutTests/platform/mac-wk2/accessibility/aria-visible-element-roles-expected.txt
+++ b/LayoutTests/platform/mac-wk2/accessibility/aria-visible-element-roles-expected.txt

 <ol hidden="" aria-hidden="false" id="ol"></ol>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup
+    AXSubrole: AXContentList

 <ul hidden="" aria-hidden="false" id="ul"></ul>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup
+    AXSubrole: AXContentList


Dito.

 <menu hidden="" aria-hidden="false" type="toolbar" id="menu-toolbar"></menu>
-    AXRole: AXList
-    computedRoleString: list
+    AXRole: AXGroup
+    AXSubrole: AXContentList

Dito.

--- a/LayoutTests/platform/wpe/accessibility/aria-visible-element-roles-expected.txt
+++ b/LayoutTests/platform/wpe/accessibility/aria-visible-element-roles-expected.txt

Same here, don't understand why these elements were role list before and now they are groups.
Comment 9 Tyler Wilcock 2023-05-24 10:58:59 PDT
(In reply to Andres Gonzalez from comment #8)
> (In reply to Tyler Wilcock from comment #7)
> > Created attachment 466440 [details]
> > Patch
> 
> @@ -516,8 +535,9 @@ bool AccessibilityNodeObject::canHaveChildren() const
>  {
>      // If this is an AccessibilityRenderObject, then it's okay if this
> object
>      // doesn't have a node - there are some renderers that don't have
> associated
> -    // nodes, like scroll areas and css-generated text.
> -    if (!node() && !isAccessibilityRenderObject())
> +    // nodes, like scroll areas and css-generated text. However, if there
> also
> +    // no renderer, we should bail.
> +    if (!renderer() && !node())
>          return false;
> 
> Can we really have this scenario? An AccessibilityNodeObject that has
> neither renderer nor node associated with it? If this is not a possible,
> expected state, maybe we should add an ASSERT here.'
These are both WeakPtrs, so the intention is to catch the case where the renderer and node in those WeakPtrs have been destroyed but the AX object has not been cleaned up yet (and thus it should not be allowed to have children, and should compute AccessibilityObjectInclusion::IgnoreObject).

> ---
> a/LayoutTests/platform/glib/accessibility/aria-visible-element-roles-
> expected.txt
> +++
> b/LayoutTests/platform/glib/accessibility/aria-visible-element-roles-
> expected.txt
> @@ -105,12 +105,10 @@ This test ensures ARIA visible elements (e.g. those
> with both `hidden` and `aria
>      AXRole: AXDescriptionList
> 
>  <ol hidden="" aria-hidden="false" id="ol"></ol>
> -    AXRole: AXList
> -    computedRoleString: list
> +    AXRole: AXGroup
> 
>  <ul hidden="" aria-hidden="false" id="ul"></ul>
> -    AXRole: AXList
> -    computedRoleString: list
> +    AXRole: AXGroup
> 
> Doesn't this mean we are breaking something for ATSPI? What it was a list
> before, now is a group.
> 
> Same here, don't understand why these elements were role list before and now
> they are groups.
Addressed this in my last reply:

Yes, all intentional. Because we now properly create display:contents lists as AccessibilityList rather than AccessibilityNodeObject, the more stringent "is this a list" heuristics are applied, and "ARIA visible" lists do not pass that test (because nothing is visibly rendered).
Comment 10 Tyler Wilcock 2023-05-24 12:01:29 PDT
Created attachment 466484 [details]
Patch
Comment 11 Tyler Wilcock 2023-05-24 12:02:29 PDT
> > Can we really have this scenario? An AccessibilityNodeObject that has
> > neither renderer nor node associated with it? If this is not a possible,
> > expected state, maybe we should add an ASSERT here.'
> These are both WeakPtrs, so the intention is to catch the case where the
> renderer and node in those WeakPtrs have been destroyed but the AX object
> has not been cleaned up yet (and thus it should not be allowed to have
> children, and should compute AccessibilityObjectInclusion::IgnoreObject).
I decided to just remove these checks in the latest patch. These objects should be cleaned up before either of these methods are called.
Comment 12 Andres Gonzalez 2023-05-25 02:46:45 PDT
(In reply to Tyler Wilcock from comment #9)
> (In reply to Andres Gonzalez from comment #8)
> > (In reply to Tyler Wilcock from comment #7)
> > > Created attachment 466440 [details]
> > > Patch
> > 
> > @@ -516,8 +535,9 @@ bool AccessibilityNodeObject::canHaveChildren() const
> >  {
> >      // If this is an AccessibilityRenderObject, then it's okay if this
> > object
> >      // doesn't have a node - there are some renderers that don't have
> > associated
> > -    // nodes, like scroll areas and css-generated text.
> > -    if (!node() && !isAccessibilityRenderObject())
> > +    // nodes, like scroll areas and css-generated text. However, if there
> > also
> > +    // no renderer, we should bail.
> > +    if (!renderer() && !node())
> >          return false;
> > 
> > Can we really have this scenario? An AccessibilityNodeObject that has
> > neither renderer nor node associated with it? If this is not a possible,
> > expected state, maybe we should add an ASSERT here.'
> These are both WeakPtrs, so the intention is to catch the case where the
> renderer and node in those WeakPtrs have been destroyed but the AX object
> has not been cleaned up yet (and thus it should not be allowed to have
> children, and should compute AccessibilityObjectInclusion::IgnoreObject).
> 
> > ---
> > a/LayoutTests/platform/glib/accessibility/aria-visible-element-roles-
> > expected.txt
> > +++
> > b/LayoutTests/platform/glib/accessibility/aria-visible-element-roles-
> > expected.txt
> > @@ -105,12 +105,10 @@ This test ensures ARIA visible elements (e.g. those
> > with both `hidden` and `aria
> >      AXRole: AXDescriptionList
> > 
> >  <ol hidden="" aria-hidden="false" id="ol"></ol>
> > -    AXRole: AXList
> > -    computedRoleString: list
> > +    AXRole: AXGroup
> > 
> >  <ul hidden="" aria-hidden="false" id="ul"></ul>
> > -    AXRole: AXList
> > -    computedRoleString: list
> > +    AXRole: AXGroup
> > 
> > Doesn't this mean we are breaking something for ATSPI? What it was a list
> > before, now is a group.
> > 
> > Same here, don't understand why these elements were role list before and now
> > they are groups.
> Addressed this in my last reply:
> 
> Yes, all intentional. Because we now properly create display:contents lists
> as AccessibilityList rather than AccessibilityNodeObject, the more stringent
> "is this a list" heuristics are applied, and "ARIA visible" lists do not
> pass that test (because nothing is visibly rendered).

Sorry that I missed your previous comment and repeated the question. This is not a display:contents list :

<ul hidden="" aria-hidden="false" id="ul"></ul>

The role of this was list before and now it is group. I understand why the change causes this, but the question is whether this is change in behavior is desirable. IMO, If we are exposing this <ul> at all, it should be a list (empty list), not a generic group.
Comment 13 Tyler Wilcock 2023-05-25 09:35:01 PDT
> Sorry that I missed your previous comment and repeated the question. This is
> not a display:contents list :
> 
> <ul hidden="" aria-hidden="false" id="ul"></ul>
> 
> The role of this was list before and now it is group. I understand why the
> change causes this, but the question is whether this is change in behavior
> is desirable. IMO, If we are exposing this <ul> at all, it should be a list
> (empty list), not a generic group.
That one is hidden (not visibly rendered, same for its descendants) but "ARIA visible" (aria-hidden="false"). Because nothing is rendered, it also fails the heuristics for "is this a list" (as it should). It's likely that we need to remove this entire notion of exposing ARIA visible (non-rendered) things anyways -- no other browser does.
Comment 14 Tyler Wilcock 2023-05-26 18:28:06 PDT
Created attachment 466511 [details]
Patch
Comment 15 Tyler Wilcock 2023-05-28 12:59:35 PDT
Created attachment 466526 [details]
Patch
Comment 16 EWS 2023-05-28 17:58:26 PDT
Committed 264644@main (ac7dd8d670b1): <https://commits.webkit.org/264644@main>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 466526 [details].